Browse Source

Merge pull request #640 from PixiEditor/vector-layers

Added support for vector layers and shape snapping
Krzysztof Krysiński 10 months ago
parent
commit
9dbb3a080a
100 changed files with 2740 additions and 667 deletions
  1. 2 1
      src/ChunkyImageLib/ChunkyImage.cs
  2. 2 2
      src/ChunkyImageLib/ChunkyImageEx.cs
  3. 47 22
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  4. 147 13
      src/ChunkyImageLib/Operations/EllipseHelper.cs
  5. 62 18
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  6. 6 2
      src/ChunkyImageLib/Operations/OperationHelper.cs
  7. 0 18
      src/PixiEditor.AvaloniaUI.GraphView/PixiEditor.AvaloniaUI.GraphView.csproj
  8. 5 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Objects/TransformObject_ChangeInfo.cs
  9. 6 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Vectors/VectorShape_ChangeInfo.cs
  10. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  11. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs
  13. 15 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  14. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  15. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyVectorNode.cs
  16. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ITransformableObject.cs
  17. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyEllipseData.cs
  18. 11 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyLineData.cs
  19. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyRectangleData.cs
  20. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  21. 58 131
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  22. 129 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  23. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  24. 0 55
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs
  25. 102 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  26. 132 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  27. 0 49
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsData.cs
  28. 81 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  29. 98 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  30. 0 24
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeData.cs
  31. 33 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  32. 4 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  33. 3 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  34. 8 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  35. 5 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  36. 6 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  37. 37 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  38. 167 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  39. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IRasterizable.cs
  40. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  41. 49 10
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  42. 13 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterEllipse_UpdateableChange.cs
  43. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs
  44. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterRectangle_UpdateableChange.cs
  45. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  46. 15 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  47. 31 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  48. 0 202
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  49. 450 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  50. 2 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs
  51. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  52. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  53. 26 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs
  54. 11 13
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  55. 128 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs
  56. 99 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  57. 36 29
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  58. 1 0
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  59. 51 4
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  60. 1 1
      src/PixiEditor.DrawingApi.Core/Bridge/DrawingBackendApi.cs
  61. 1 1
      src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs
  62. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IMatrix3x3Implementation.cs
  63. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/ICanvasImplementation.cs
  64. 15 0
      src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs
  65. 2 0
      src/PixiEditor.DrawingApi.Core/Surfaces/Canvas.cs
  66. 7 1
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs
  67. 6 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaMatrixImplementation.cs
  68. 1 1
      src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs
  69. 11 0
      src/PixiEditor.SVG/Elements/SvgCircle.cs
  70. 13 0
      src/PixiEditor.SVG/Elements/SvgEllipse.cs
  71. 13 0
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  72. 22 0
      src/PixiEditor.SVG/Elements/SvgImage.cs
  73. 12 0
      src/PixiEditor.SVG/Elements/SvgLine.cs
  74. 14 0
      src/PixiEditor.SVG/Elements/SvgMask.cs
  75. 8 0
      src/PixiEditor.SVG/Elements/SvgPolyline.cs
  76. 12 0
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  77. 15 0
      src/PixiEditor.SVG/Elements/SvgRectangle.cs
  78. 10 0
      src/PixiEditor.SVG/Enums/SvgImageRenderingType.cs
  79. 6 0
      src/PixiEditor.SVG/Features/IElementContainer.cs
  80. 8 0
      src/PixiEditor.SVG/Features/IFillable.cs
  81. 9 0
      src/PixiEditor.SVG/Features/IStrokable.cs
  82. 8 0
      src/PixiEditor.SVG/Features/ITransformable.cs
  83. 9 0
      src/PixiEditor.SVG/Helpers/StringExtensions.cs
  84. 14 0
      src/PixiEditor.SVG/PixiEditor.SVG.csproj
  85. 3 0
      src/PixiEditor.SVG/README.md
  86. 57 0
      src/PixiEditor.SVG/SvgDocument.cs
  87. 50 0
      src/PixiEditor.SVG/SvgElement.cs
  88. 27 0
      src/PixiEditor.SVG/SvgProperty.cs
  89. 11 0
      src/PixiEditor.SVG/Units/SvgArray.cs
  90. 41 0
      src/PixiEditor.SVG/Units/SvgColorUnit.cs
  91. 18 0
      src/PixiEditor.SVG/Units/SvgEnumUnit.cs
  92. 20 0
      src/PixiEditor.SVG/Units/SvgLinkUnit.cs
  93. 45 0
      src/PixiEditor.SVG/Units/SvgNumericUnit.cs
  94. 15 0
      src/PixiEditor.SVG/Units/SvgStringUnit.cs
  95. 31 0
      src/PixiEditor.SVG/Units/SvgTransformUnit.cs
  96. 6 0
      src/PixiEditor.SVG/Units/SvgUnit.cs
  97. 8 0
      src/PixiEditor.UI.Common/Accents/Base.axaml
  98. 2 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  99. 3 0
      src/PixiEditor.Zoombox/Zoombox.cs
  100. 31 0
      src/PixiEditor.sln

+ 2 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -580,12 +580,13 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
+        double rotationRad = 0,
         Paint? paint = null)
     {
         lock (lockObject)
         {
             ThrowIfDisposed();
-            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, paint);
+            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, paint);
             EnqueueOperation(operation);
         }
     }

+ 2 - 2
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -48,7 +48,7 @@ public static class IReadOnlyChunkyImageEx
         Func<VecI, ChunkResolution, DrawingSurface, VecI, Paint?, bool> drawingFunc,
         Paint? paint = null)
     {
-        surface.Canvas.Save();
+        int count = surface.Canvas.Save();
         surface.Canvas.ClipRect(RectD.Create(pos, fullResRegion.Size));
 
         VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
@@ -65,6 +65,6 @@ public static class IReadOnlyChunkyImageEx
             }
         }
 
-        surface.Canvas.Restore();
+        surface.Canvas.RestoreToCount(count);
     }
 }

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

@@ -8,6 +8,7 @@ namespace ChunkyImageLib.DataHolders;
 public struct ShapeCorners
 {
     private const double epsilon = 0.001;
+
     public ShapeCorners(VecD center, VecD size)
     {
         TopLeft = center - size / 2;
@@ -15,6 +16,7 @@ public struct ShapeCorners
         BottomRight = center + size / 2;
         BottomLeft = center + new VecD(-size.X / 2, size.Y / 2);
     }
+
     public ShapeCorners(RectD rect)
     {
         TopLeft = rect.TopLeft;
@@ -22,10 +24,12 @@ public struct ShapeCorners
         BottomRight = rect.BottomRight;
         BottomLeft = rect.BottomLeft;
     }
+
     public VecD TopLeft { get; set; }
     public VecD TopRight { get; set; }
     public VecD BottomLeft { get; set; }
     public VecD BottomRight { get; set; }
+
     public bool IsInverted
     {
         get
@@ -34,9 +38,11 @@ public struct ShapeCorners
             var right = TopRight - BottomRight;
             var bottom = BottomRight - BottomLeft;
             var left = BottomLeft - TopLeft;
-            return Math.Sign(top.Cross(right)) + Math.Sign(right.Cross(bottom)) + Math.Sign(bottom.Cross(left)) + Math.Sign(left.Cross(top)) < 0;
+            return Math.Sign(top.Cross(right)) + Math.Sign(right.Cross(bottom)) + Math.Sign(bottom.Cross(left)) +
+                Math.Sign(left.Cross(top)) < 0;
         }
     }
+
     public bool IsLegal
     {
         get
@@ -48,7 +54,8 @@ public struct ShapeCorners
             var bottom = BottomRight - BottomLeft;
             var left = BottomLeft - TopLeft;
             var topRight = Math.Sign(top.Cross(right));
-            return topRight == Math.Sign(right.Cross(bottom)) && topRight == Math.Sign(bottom.Cross(left)) && topRight == Math.Sign(left.Cross(top));
+            return topRight == Math.Sign(right.Cross(bottom)) && topRight == Math.Sign(bottom.Cross(left)) &&
+                   topRight == Math.Sign(left.Cross(top));
         }
     }
 
@@ -59,32 +66,34 @@ public struct ShapeCorners
     {
         get
         {
-            Span<VecD> lengths = stackalloc[] 
+            Span<VecD> lengths = stackalloc[]
             {
-                TopLeft - TopRight,
-                TopRight - BottomRight,
-                BottomRight - BottomLeft,
-                BottomLeft - TopLeft,
-                TopLeft - BottomRight,
-                TopRight - BottomLeft
+                TopLeft - TopRight, TopRight - BottomRight, BottomRight - BottomLeft, BottomLeft - TopLeft,
+                TopLeft - BottomRight, TopRight - BottomLeft
             };
             foreach (VecD vec in lengths)
             {
                 if (vec.LengthSquared < epsilon * epsilon)
                     return true;
             }
+
             return false;
         }
     }
-    public bool HasNaNOrInfinity => TopLeft.IsNaNOrInfinity() || TopRight.IsNaNOrInfinity() || BottomLeft.IsNaNOrInfinity() || BottomRight.IsNaNOrInfinity();
+
+    public bool HasNaNOrInfinity => TopLeft.IsNaNOrInfinity() || TopRight.IsNaNOrInfinity() ||
+                                    BottomLeft.IsNaNOrInfinity() || BottomRight.IsNaNOrInfinity();
+
     public bool IsRect => Math.Abs((TopLeft - BottomRight).Length - (TopRight - BottomLeft).Length) < epsilon;
     public VecD RectSize => new((TopLeft - TopRight).Length, (TopLeft - BottomLeft).Length);
     public VecD RectCenter => (TopLeft - BottomRight) / 2 + BottomRight;
+
     public double RectRotation =>
-        (TopLeft - TopRight).Cross(TopLeft - BottomLeft) > 0 ?
-        RectSize.CCWAngleTo(BottomRight - TopLeft) :
-        RectSize.CCWAngleTo(BottomLeft - TopRight);
-    public bool IsSnappedToPixels
+        (TopLeft - TopRight).Cross(TopLeft - BottomLeft) > 0
+            ? RectSize.CCWAngleTo(BottomRight - TopLeft)
+            : RectSize.CCWAngleTo(BottomLeft - TopRight);
+
+    public bool IsAlignedToPixels
     {
         get
         {
@@ -96,6 +105,7 @@ public struct ShapeCorners
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
         }
     }
+
     public RectD AABBBounds
     {
         get
@@ -120,7 +130,8 @@ public struct ShapeCorners
         var deltaBottomRight = point - BottomRight;
         var deltaBottomLeft = point - BottomLeft;
 
-        if (deltaTopRight.IsNaNOrInfinity() || deltaTopLeft.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity())
+        if (deltaTopRight.IsNaNOrInfinity() || deltaTopLeft.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity() ||
+            deltaBottomRight.IsNaNOrInfinity())
             return false;
 
         var crossTop = Math.Sign(top.Cross(deltaTopLeft));
@@ -186,13 +197,14 @@ public struct ShapeCorners
     }
 
     public static bool operator !=(ShapeCorners left, ShapeCorners right) => !(left == right);
-    public static bool operator == (ShapeCorners left, ShapeCorners right)
+
+    public static bool operator ==(ShapeCorners left, ShapeCorners right)
     {
-        return 
-           left.TopLeft == right.TopLeft &&
-           left.TopRight == right.TopRight &&
-           left.BottomLeft == right.BottomLeft &&
-           left.BottomRight == right.BottomRight;
+        return
+            left.TopLeft == right.TopLeft &&
+            left.TopRight == right.TopRight &&
+            left.BottomLeft == right.BottomLeft &&
+            left.BottomRight == right.BottomRight;
     }
 
     public bool AlmostEquals(ShapeCorners other, double epsilon = 0.001)
@@ -203,7 +215,7 @@ public struct ShapeCorners
             BottomLeft.AlmostEquals(other.BottomLeft, epsilon) &&
             BottomRight.AlmostEquals(other.BottomRight, epsilon);
     }
-    
+
     public bool Intersects(RectD rect)
     {
         // Get all corners
@@ -245,4 +257,17 @@ public struct ShapeCorners
         // All projections overlap, so the shapes intersect
         return true;
     }
+
+    public ShapeCorners WithMatrix(Matrix3X3 transformationMatrix)
+    {
+        ShapeCorners corners = new ShapeCorners
+        {
+            TopLeft = transformationMatrix.MapPoint(TopLeft),
+            TopRight = transformationMatrix.MapPoint(TopRight),
+            BottomLeft = transformationMatrix.MapPoint(BottomLeft),
+            BottomRight = transformationMatrix.MapPoint(BottomRight)
+        };
+
+        return corners;
+    }
 }

+ 147 - 13
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -1,14 +1,17 @@
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 public class EllipseHelper
 {
     /// <summary>
     /// Separates the ellipse's inner area into a bunch of horizontal lines and one big rectangle for drawing.
     /// </summary>
-    public static (List<VecI> lines, RectI rect) SplitEllipseFillIntoRegions(IReadOnlyList<VecI> ellipse, RectI ellipseBounds)
+    public static (List<VecI> lines, RectI rect) SplitEllipseFillIntoRegions(IReadOnlyList<VecI> ellipse,
+        RectI ellipseBounds)
     {
         if (ellipse.Count == 0)
             return (new(), RectI.Empty);
@@ -51,14 +54,15 @@ public class EllipseHelper
                 }
             }
         }
+
         return (lines, inscribedRect);
     }
-    
+
     /// <summary>
     /// Splits the ellipse into a bunch of horizontal lines.
     /// The resulting list contains consecutive pairs of <see cref="VecI"/>s, each pair has one for the start of the line and one for the end.
     /// </summary>
-    public static List<VecI> SplitEllipseIntoLines(List<VecI> ellipse)
+    public static List<VecI> SplitEllipseIntoLines(HashSet<VecI> ellipse)
     {
         List<VecI> lines = new();
         var sorted = ellipse.OrderBy(
@@ -79,25 +83,31 @@ public class EllipseHelper
                 minX = int.MaxValue;
                 maxX = int.MinValue;
             }
+
             minX = Math.Min(point.X, minX);
             maxX = Math.Max(point.X, maxX);
             prev = point;
         }
+
         if (prev != null)
         {
             lines.Add(new(minX, prev.Value.Y));
             lines.Add(new(maxX, prev.Value.Y));
         }
+
         return lines;
     }
-    
-    public static List<VecI> GenerateEllipseFromRect(RectI rect)
+
+    public static HashSet<VecI> GenerateEllipseFromRect(RectI rect, double rotationRad = 0)
     {
         if (rect.IsZeroOrNegativeArea)
             return new();
         float radiusX = (rect.Width - 1) / 2.0f;
         float radiusY = (rect.Height - 1) / 2.0f;
-        return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
+        if (rotationRad == 0)
+            return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
+        
+        return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
     }
 
     /// <summary>
@@ -114,14 +124,14 @@ public class EllipseHelper
     /// Center is at (2; 2). It's a place where 4 pixels meet
     /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
     /// </summary>
-    public static List<VecI> GenerateMidpointEllipse(
+    public static HashSet<VecI> GenerateMidpointEllipse(
         double halfWidth,
         double halfHeight,
         double centerX,
         double centerY,
-        List<VecI>? listToFill = null)
+        HashSet<VecI>? listToFill = null)
     {
-        listToFill ??= new List<VecI>();
+        listToFill ??= new HashSet<VecI>();
         if (halfWidth < 1 || halfHeight < 1)
         {
             AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
@@ -156,8 +166,7 @@ public class EllipseHelper
             double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
             double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
             currentSlope = -(derivativeX / derivativeY);
-        }
-        while (currentSlope > -1 && currentY - centerY > 0.5);
+        } while (currentSlope > -1 && currentY - centerY > 0.5);
 
         // from PI/4 to 0
         while (currentY - centerY >= 0)
@@ -176,7 +185,122 @@ public class EllipseHelper
         return listToFill;
     }
 
-    private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY, List<VecI> coordinates)
+    // This function works, but honestly Skia produces better results, and it doesn't require so much
+    // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
+    // make it better, and it will be useful.
+    // Desmos with all the math https://www.desmos.com/calculator/m9lgg7s9zu
+    private static HashSet<VecI> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX,
+        double centerY, double rotationRad)
+    {
+        var listToFill = new HashSet<VecI>();
+
+        // formula ((x - h)cos(tetha) + (y - k)sin(tetha))^2 / a^2 + (-(x-h)sin(tetha)+(y-k)cos(tetha))^2 / b^2 = 1
+
+        //double topMostTetha = GetTopMostAlpha(halfWidth, halfHeight, rotationRad);
+
+        //VecD possiblyTopmostPoint = GetTethaPoint(topMostTetha, halfWidth, halfHeight, rotationRad);
+        //VecD possiblyMinPoint = GetTethaPoint(topMostTetha + Math.PI, halfWidth, halfHeight, rotationRad);
+
+        // less than, because y grows downwards
+        //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
+        
+        //rotationRad = double.Round(rotationRad, 1);
+
+        double currentTetha = 0;
+
+        double tethaStep = 0.001;
+
+        VecI[] lastPoints = new VecI[2];
+
+        do
+        {
+            VecD point = GetTethaPoint(currentTetha, halfWidth, halfHeight, rotationRad);
+            VecI floored = new((int)Math.Floor(point.X + centerX), (int)Math.Floor(point.Y + centerY));
+
+            AddPoint(listToFill, floored, lastPoints);
+
+            currentTetha += tethaStep;
+        } while (currentTetha < Math.PI * 2);
+        
+        return listToFill;
+    }
+
+    private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
+    {
+        if(!listToFill.Add(floored)) return;
+
+        if (lastPoints[0] == default)
+        {
+            lastPoints[0] = floored;
+            return;
+        }
+
+        if (lastPoints[1] == default)
+        {
+            lastPoints[1] = floored;
+            return;
+        }
+
+        if (IsLShape(lastPoints, floored))
+        {
+            listToFill.Remove(lastPoints[1]);
+
+            lastPoints[0] = floored;
+            lastPoints[1] = default;
+            
+            return;
+        }
+
+        lastPoints[0] = lastPoints[1];
+        lastPoints[1] = floored;
+    }
+
+    private static bool IsLShape(VecI[] points, VecI third)
+    {
+        VecI first = points[0];
+        VecI second = points[1];
+        return first.X != third.X && first.Y != third.Y && (second - first).TaxicabLength == 1 &&
+               (second - third).TaxicabLength == 1;
+    }
+
+    private static bool IsInsideEllipse(double x, double y, double centerX, double centerY, double halfWidth,
+        double halfHeight, double rotationRad)
+    {
+        double lhs = Math.Pow(x * Math.Cos(rotationRad) + y * Math.Sin(rotationRad), 2) / Math.Pow(halfWidth, 2);
+        double rhs = Math.Pow(-x * Math.Sin(rotationRad) + y * Math.Cos(rotationRad), 2) / Math.Pow(halfHeight, 2);
+
+        return lhs + rhs <= 1;
+    }
+
+    private static VecD GetDerivative(double x, double halfWidth, double halfHeight, double rotationRad, double tetha)
+    {
+        double xDerivative = halfWidth * Math.Cos(tetha) * Math.Cos(rotationRad) -
+                             halfHeight * Math.Sin(tetha) * Math.Sin(rotationRad);
+        double yDerivative = halfWidth * Math.Cos(tetha) * Math.Sin(rotationRad) +
+                             halfHeight * Math.Sin(tetha) * Math.Cos(rotationRad);
+
+        return new VecD(xDerivative, yDerivative);
+    }
+
+    private static VecD GetTethaPoint(double alpha, double halfWidth, double halfHeight, double rotation)
+    {
+        double x =
+            (halfWidth * Math.Cos(alpha) * Math.Cos(rotation) - halfHeight * Math.Sin(alpha) * Math.Sin(rotation));
+        double y = halfWidth * Math.Cos(alpha) * Math.Sin(rotation) + halfHeight * Math.Sin(alpha) * Math.Cos(rotation);
+
+        return new VecD(x, y);
+    }
+
+    private static double GetTopMostAlpha(double halfWidth, double halfHeight, double rotationRad)
+    {
+        if (rotationRad == 0)
+            return 0;
+        double tethaRot = Math.Cos(rotationRad) / Math.Sin(rotationRad);
+        return Math.Atan((halfHeight * tethaRot) / halfWidth);
+    }
+
+    private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY,
+        HashSet<VecI> coordinates)
     {
         int left = (int)Math.Floor(centerX - halfWidth);
         int top = (int)Math.Floor(centerY - halfHeight);
@@ -198,7 +322,7 @@ public class EllipseHelper
         }
     }
 
-    private static void AddRegionPoints(List<VecI> coordinates, double x, double xc, double y, double yc)
+    private static void AddRegionPoints(HashSet<VecI> coordinates, double x, double xc, double y, double yc)
     {
         int xFloor = (int)Math.Floor(x);
         int yFloor = (int)Math.Floor(y);
@@ -220,4 +344,14 @@ public class EllipseHelper
             coordinates.Add(new VecI(xFloor, yFloorInv));
         }
     }
+
+    public static VectorPath GenerateEllipseVectorFromRect(RectI location)
+    {
+        VectorPath path = new();
+        path.AddOval(location);
+       
+        path.Close();
+        
+        return path;
+    }
 }

+ 62 - 18
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -15,20 +15,24 @@ internal class EllipseOperation : IMirroredDrawOperation
     private readonly Color strokeColor;
     private readonly Color fillColor;
     private readonly int strokeWidth;
+    private readonly double rotation;
     private readonly Paint paint;
     private bool init = false;
     private VectorPath? outerPath;
     private VectorPath? innerPath;
+    
+    private VectorPath ellipseOutline;
     private Point[]? ellipse;
     private Point[]? ellipseFill;
     private RectI? ellipseFillRect;
 
-    public EllipseOperation(RectI location, Color strokeColor, Color fillColor, int strokeWidth, Paint? paint = null)
+    public EllipseOperation(RectI location, Color strokeColor, Color fillColor, int strokeWidth, double rotationRad, Paint? paint = null)
     {
         this.location = location;
         this.strokeColor = strokeColor;
         this.fillColor = fillColor;
         this.strokeWidth = strokeWidth;
+        this.rotation = rotationRad;
         this.paint = paint?.Clone() ?? new Paint();
     }
 
@@ -37,12 +41,21 @@ internal class EllipseOperation : IMirroredDrawOperation
         init = true;
         if (strokeWidth == 1)
         {
-            var ellipseList = EllipseHelper.GenerateEllipseFromRect(location);
-            ellipse = ellipseList.Select(a => new Point(a)).ToArray();
-            if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+            if (Math.Abs(rotation) < 0.001)
+            {
+                var ellipseList = EllipseHelper.GenerateEllipseFromRect(location);
+
+                ellipse = ellipseList.Select(a => new Point(a)).ToArray();
+
+                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                {
+                    (var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), location);
+                    ellipseFill = fill.Select(a => new Point(a)).ToArray();
+                }
+            }
+            else
             {
-                (var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList, location);
-                ellipseFill = fill.Select(a => new Point(a)).ToArray();
+                ellipseOutline = EllipseHelper.GenerateEllipseVectorFromRect(location);
             }
         }
         else
@@ -67,25 +80,52 @@ internal class EllipseOperation : IMirroredDrawOperation
 
         if (strokeWidth == 1)
         {
-            if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+            if (Math.Abs(rotation) < 0.001)
             {
-                paint.Color = fillColor;
-                surf.Canvas.DrawPoints(PointMode.Lines, ellipseFill!, paint);
-                surf.Canvas.DrawRect(ellipseFillRect!.Value, paint);
+                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                {
+                    paint.Color = fillColor;
+                    surf.Canvas.DrawPoints(PointMode.Lines, ellipseFill!, paint);
+                    surf.Canvas.DrawRect(ellipseFillRect!.Value, paint);
+                }
+                
+                paint.Color = strokeColor;
+                paint.StrokeWidth = 1f;
+                surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
+            }
+            else
+            {
+                surf.Canvas.Save();
+                surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
+                
+                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                {
+                    paint.Color = fillColor;
+                    paint.Style = PaintStyle.Fill;
+                    surf.Canvas.DrawPath(ellipseOutline, paint);
+                }
+                
+                paint.Color = strokeColor;
+                paint.Style = PaintStyle.Stroke;
+                paint.StrokeWidth = 1f;
+                
+                surf.Canvas.DrawPath(ellipseOutline, paint);
+
+                surf.Canvas.Restore();
             }
-            paint.Color = strokeColor;
-            surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
         }
         else
         {
             if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
             {
                 surf.Canvas.Save();
+                surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.ClipPath(innerPath!);
                 surf.Canvas.DrawColor(fillColor, paint.BlendMode);
                 surf.Canvas.Restore();
             }
             surf.Canvas.Save();
+            surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
             surf.Canvas.DrawColor(strokeColor, paint.BlendMode);
@@ -96,14 +136,18 @@ internal class EllipseOperation : IMirroredDrawOperation
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        var chunks = OperationHelper.FindChunksTouchingEllipse
-            (location.Center, location.Width / 2.0, location.Height / 2.0, ChunkyImage.FullChunkSize);
+        ShapeCorners corners = new((RectD)location);
+        corners = corners.AsRotated(rotation, (VecD)location.Center);
+        RectI bounds = (RectI)corners.AABBBounds.RoundOutwards();
+        
+        var chunks = OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
         if (fillColor.A == 0)
         {
-            chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
-                (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize));
+             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
+                (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize, rotation));
         }
-        return new AffectedArea(chunks, location);
+        
+        return new AffectedArea(chunks, bounds);
     }
 
     public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
@@ -113,7 +157,7 @@ internal class EllipseOperation : IMirroredDrawOperation
             newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
             newLocation = (RectI)newLocation.ReflectY((double)horAxisY).Round();
-        return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, paint);
+        return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, rotation, paint);
     }
 
     public void Dispose()

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

@@ -176,15 +176,19 @@ public static class OperationHelper
         return chunks;
     }
 
-    public static HashSet<VecI> FindChunksFullyInsideEllipse(VecD pos, double radiusX, double radiusY, int chunkSize)
+    public static HashSet<VecI> FindChunksFullyInsideEllipse(VecD pos, double radiusX, double radiusY, int chunkSize,
+        double rotation)
     {
         double stretchX = radiusX / radiusY;
         var (left, right) = CreateStretchedHexagon(pos, radiusY, stretchX);
+        left = left.AsRotated(rotation, pos);
+        right = right.AsRotated(rotation, pos);
+        
         var chunks = FindChunksFullyInsideQuadrilateral(left, chunkSize);
         chunks.UnionWith(FindChunksFullyInsideQuadrilateral(right, chunkSize));
         return chunks;
     }
-
+    
     public static HashSet<VecI> FindChunksTouchingQuadrilateral(ShapeCorners corners, int chunkSize)
     {
         if (corners.IsRect && Math.Abs(corners.RectRotation) < 0.0001)

+ 0 - 18
src/PixiEditor.AvaloniaUI.GraphView/PixiEditor.AvaloniaUI.GraphView.csproj

@@ -1,18 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
-      <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\PixiEditor.Zoombox\PixiEditor.Zoombox.csproj" />
-    </ItemGroup>
-
-</Project>

+ 5 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Objects/TransformObject_ChangeInfo.cs

@@ -0,0 +1,5 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Objects;
+
+public record TransformObject_ChangeInfo(Guid NodeGuid, AffectedArea Area) : IChangeInfo;

+ 6 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Vectors/VectorShape_ChangeInfo.cs

@@ -0,0 +1,6 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+
+    public record VectorShape_ChangeInfo(Guid LayerId, AffectedArea Affected) : IChangeInfo;

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -76,7 +76,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             throw new ArgumentException(@"The given guid does not belong to a layer.", nameof(layerGuid));
 
 
-        RectI? tightBounds = layer.GetTightBounds(frame);
+        RectI? tightBounds = (RectI)layer.GetTightBounds(frame);
 
         if (tightBounds is null)
             return null;
@@ -126,7 +126,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             throw new ArgumentException(@"The given guid does not belong to a layer.", nameof(layerGuid));
 
 
-        return layer.GetTightBounds(frame);
+        return (RectI)layer.GetTightBounds(frame);
     }
 
     public void ForEveryReadonlyMember(Action<IReadOnlyStructureNode> action) =>
@@ -263,7 +263,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// <returns>True if the node could be found, otherwise false.</returns>
     public bool TryFindNode<T>(Guid id, out T node) where T : Node
     {
-        node = (T?)NodeGraph.Nodes.FirstOrDefault(x => x.Id == id) ?? default;
+        node = (T?)NodeGraph.Nodes.FirstOrDefault(x => x.Id == id && x is T) ?? default;
         return node != null;
     }
 

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs

@@ -0,0 +1,9 @@
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface IPreviewRenderable
+{
+    public bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame);
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs

@@ -1,5 +1,5 @@
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
-public interface IReadOnlyLayerNode : IReadOnlyStructureNode
+public interface IReadOnlyLayerNode : IReadOnlyStructureNode, IPreviewRenderable
 {
 }

+ 15 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs

@@ -0,0 +1,15 @@
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface IReadOnlyShapeVectorData
+{
+    public Matrix3X3 TransformationMatrix { get; }
+    public Color StrokeColor { get; }
+    public Color FillColor { get; }
+    public int StrokeWidth { get; }
+    public RectD GeometryAABB { get; }
+    public RectD TransformedAABB { get; }
+}

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs

@@ -14,7 +14,7 @@ public interface IReadOnlyStructureNode : IReadOnlyNode
     public InputProperty<Texture?> CustomMask { get; }
     public InputProperty<bool> MaskIsVisible { get; }
     public string MemberName { get; set; }
-    public RectI? GetTightBounds(KeyFrameTime frameTime);
-    
+    public RectD? GetTightBounds(KeyFrameTime frameTime);
     public ChunkyImage? EmbeddedMask { get; }
+    public ShapeCorners GetTransformationCorners(KeyFrameTime frameTime);
 }

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyVectorNode.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface IReadOnlyVectorNode : IReadOnlyLayerNode
+{
+    public IReadOnlyShapeVectorData? ShapeData { get; }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ITransformableObject.cs

@@ -0,0 +1,9 @@
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface ITransformableObject
+{
+    public Matrix3X3 TransformationMatrix { get; set; }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyEllipseData.cs

@@ -0,0 +1,9 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyEllipseData : IReadOnlyShapeVectorData
+{
+    public VecD Center { get; }
+    public VecD Radius { get; }
+}

+ 11 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyLineData.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyLineData : IReadOnlyShapeVectorData
+{
+    public VecD Start { get; }
+    public VecD End { get; }
+    public VecD TransformedStart { get; set; }
+    public VecD TransformedEnd { get; set; }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyRectangleData.cs

@@ -0,0 +1,9 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyRectangleData : IReadOnlyShapeVectorData
+{
+    public VecD Center { get; }
+    public VecD Size { get; }
+}

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -102,7 +102,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
         return Output.Value;
     }
 
-    public override RectI? GetTightBounds(KeyFrameTime frameTime)
+    public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
         RectI bounds = new RectI();
         if(Content.Connection != null)
@@ -111,7 +111,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
             {
                 if (n is ImageLayerNode imageLayerNode)
                 {
-                    RectI? imageBounds = imageLayerNode.GetTightBounds(frameTime);
+                    RectI? imageBounds = (RectI?)imageLayerNode.GetTightBounds(frameTime);
                     if (imageBounds != null)
                     {
                         bounds = bounds.Union(imageBounds.Value);
@@ -121,10 +121,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
                 return true;
             });
             
-            return bounds;
+            return (RectD)bounds;
         }
         
-        return RectI.Create(0, 0, Content.Value?.Size.X ?? 0, Content.Value?.Size.Y ?? 0);
+        return (RectD)RectI.Create(0, 0, Content.Value?.Size.X ?? 0, Content.Value?.Size.Y ?? 0);
     }
 
     public HashSet<Guid> GetLayerNodeGuids()

+ 58 - 131
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -21,7 +21,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     private VecI size;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
-    
+
 
     protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces =
         new Dictionary<(ChunkResolution, int), Texture>();
@@ -51,147 +51,54 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         this.size = size;
     }
 
-    public override RectI? GetTightBounds(KeyFrameTime frameTime)
-    {
-        return GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
-    }
 
-    protected override Texture? OnExecute(RenderingContext context)
+    public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
-        {
-            Output.Value = Background.Value;
-            return Output.Value;
-        }
-
-        var frameImage = GetFrameWithImage(context.FrameTime);
-
-        blendPaint.Color = new Color(255, 255, 255, 255);
-        blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
-
-        var renderedSurface = RenderImage(frameImage.Data as ChunkyImage, context);
-
-        Output.Value = renderedSurface;
-
-        return Output.Value;
+        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
     }
 
-    private Texture RenderImage(ChunkyImage frameImage, RenderingContext context)
+    protected override Texture? OnExecute(RenderingContext context)
     {
-        bool shouldClear = Background.Value == null;
-
-        if (FilterlessOutput.Connections.Count > 0)
-        {
-            var filterlessWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 1);
-
-            if (Background.Value != null)
-            {
-                DrawBackground(filterlessWorkingSurface, context);
-                blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
-            }
-
-            DrawLayer(frameImage, context, filterlessWorkingSurface, shouldClear, useFilters: false);
-            blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
-
-            FilterlessOutput.Value = filterlessWorkingSurface;
-        }
+        var rendered = base.OnExecute(context);
 
         if (RawOutput.Connections.Count > 0)
         {
-            var rawWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 2);
-            DrawLayer(frameImage, context, rawWorkingSurface, true, useFilters: false);
+            var rawWorkingSurface = TryInitWorkingSurface(GetTargetSize(context), context, 2);
+            DrawLayer(context, rawWorkingSurface, true, useFilters: false);
 
             RawOutput.Value = rawWorkingSurface;
         }
 
-        if (Output.Connections.Count > 0)
-        {
-            var outputWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 0);
-
-            if (!HasOperations())
-            {
-                if (Background.Value != null)
-                {
-                    DrawBackground(outputWorkingSurface, context);
-                    blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
-                }
-
-                DrawLayer(frameImage, context, outputWorkingSurface, shouldClear);
-
-                Output.Value = outputWorkingSurface;
-
-                return outputWorkingSurface;
-            }
-
-            DrawLayer(frameImage, context, outputWorkingSurface, true);
-
-            // shit gets downhill with mask on big canvases, TODO: optimize
-            ApplyMaskIfPresent(outputWorkingSurface, context);
-
-            if (Background.Value != null)
-            {
-                Texture tempSurface = RequestTexture(4, outputWorkingSurface.Size, true);
-                DrawBackground(tempSurface, context);
-                ApplyRasterClip(outputWorkingSurface, tempSurface);
-                blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
-                tempSurface.DrawingSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
-                Output.Value = tempSurface;
-                return tempSurface;
-            }
-
-            Output.Value = outputWorkingSurface;
-
-            return outputWorkingSurface;
-        }
-
-        return null;
+        return rendered;
     }
 
-    protected Texture TryInitWorkingSurface(VecI imageSize, RenderingContext context, int id)
+    protected override VecI GetTargetSize(RenderingContext ctx)
     {
-        ChunkResolution targetResolution = context.ChunkResolution;
-        bool hasSurface = workingSurfaces.TryGetValue((targetResolution, id), out Texture workingSurface);
-        VecI targetSize = (VecI)(imageSize * targetResolution.Multiplier());
-
-        if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
-        {
-            workingSurfaces[(targetResolution, id)] = new Texture(targetSize);
-            workingSurface = workingSurfaces[(targetResolution, id)];
-        }
-
-        return workingSurface;
+        return (GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage).LatestSize;
     }
 
-    private void DrawLayer(ChunkyImage frameImage, RenderingContext context, Texture workingSurface, bool shouldClear,
-        bool useFilters = true)
+    protected override void DrawWithoutFilters(RenderingContext ctx, Texture workingSurface, bool shouldClear,
+        Paint paint)
     {
-        blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255));
-
-        if (useFilters && Filters.Value != null)
-        {
-            DrawWithFilters(frameImage, context, workingSurface, shouldClear);
-        }
-        else
+        var frameImage = GetFrameWithImage(ctx.FrameTime).Data as ChunkyImage;
+        if (!frameImage.DrawMostUpToDateChunkOn(
+                ctx.ChunkToUpdate,
+                ctx.ChunkResolution,
+                workingSurface.DrawingSurface,
+                ctx.ChunkToUpdate * ctx.ChunkResolution.PixelSize(),
+                blendPaint) && shouldClear)
         {
-            blendPaint.SetFilters(null);
-
-            if (!frameImage.DrawMostUpToDateChunkOn(
-                    context.ChunkToUpdate,
-                    context.ChunkResolution,
-                    workingSurface.DrawingSurface,
-                    context.ChunkToUpdate * context.ChunkResolution.PixelSize(),
-                    blendPaint) && shouldClear)
-            {
-                workingSurface.DrawingSurface.Canvas.DrawRect(CalculateDestinationRect(context), clearPaint);
-            }
+            workingSurface.DrawingSurface.Canvas.DrawRect(CalculateDestinationRect(ctx), clearPaint);
         }
     }
 
     // Draw with filters is a bit tricky since some filters sample data from chunks surrounding the chunk being drawn,
     // this is why we need to do intermediate drawing to a temporary surface and then apply filters to that surface
-    private void DrawWithFilters(ChunkyImage frameImage, RenderingContext context, Texture workingSurface,
-        bool shouldClear)
+    protected override void DrawWithFilters(RenderingContext context, Texture workingSurface,
+        bool shouldClear, Paint paint)
     {
+        var frameImage = GetFrameWithImage(context.FrameTime).Data as ChunkyImage;
+        
         VecI imageChunksSize = frameImage.LatestSize / context.ChunkResolution.PixelSize();
         bool requiresTopLeft = context.ChunkToUpdate.X > 0 || context.ChunkToUpdate.Y > 0;
         bool requiresTop = context.ChunkToUpdate.Y > 0;
@@ -240,51 +147,71 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
         if (requiresTopLeft)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(-1, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(-1, -1), paint);
         }
 
         if (requiresTop)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(0, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(0, -1), paint);
         }
 
         if (requiresLeft)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 0));
+            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 0), paint);
         }
 
         if (requiresTopRight)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, -1), paint);
         }
 
         if (requiresRight)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, 0));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, 0), paint);
         }
 
         if (requiresBottomRight)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, 1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, 1), paint);
         }
 
         if (requiresBottom)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(0, 1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(0, 1), paint);
         }
 
         if (requiresBottomLeft)
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 1), paint);
         }
 
-        DrawChunk(frameImage, context, tempSurface, new VecI(0, 0));
+        DrawChunk(frameImage, context, tempSurface, new VecI(0, 0), paint);
 
-        blendPaint.SetFilters(Filters.Value);
-        workingSurface.DrawingSurface.Canvas.DrawSurface(tempSurface.DrawingSurface, VecI.Zero, blendPaint);
+        workingSurface.DrawingSurface.Canvas.DrawSurface(tempSurface.DrawingSurface, VecI.Zero, paint);
+    }
+
+    public override bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame)
+    {
+        var img = GetLayerImageAtFrame(frame);
+        
+        if (img is null)
+        {
+            return false;
+        }
+        
+        img.DrawMostUpToDateChunkOn(
+            chunk,
+            resolution,
+            renderOn.DrawingSurface,
+            chunk * resolution.PixelSize(),
+            blendPaint);
+        
+        return true;
     }
 
-    private void DrawChunk(ChunkyImage frameImage, RenderingContext context, Texture tempSurface, VecI vecI)
+
+    private void DrawChunk(ChunkyImage frameImage, RenderingContext context, Texture tempSurface, VecI vecI,
+        Paint paint)
     {
         VecI chunkPos = context.ChunkToUpdate + vecI;
         if (frameImage.LatestOrCommittedChunkExists(chunkPos))
@@ -294,7 +221,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 context.ChunkResolution,
                 tempSurface.DrawingSurface,
                 chunkPos * context.ChunkResolution.PixelSize(),
-                blendPaint);
+                paint);
         }
     }
 
@@ -313,7 +240,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     protected override bool CacheChanged(RenderingContext context)
     {
         var frame = GetFrameWithImage(context.FrameTime);
-        
+
         return base.CacheChanged(context) || frame?.RequiresUpdate == true;
     }
 

+ 129 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -1,9 +1,138 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Helpers;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 public abstract class LayerNode : StructureNode, IReadOnlyLayerNode
 {
+    protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces =
+        new Dictionary<(ChunkResolution, int), Texture>();
+
+    protected override Texture? OnExecute(RenderingContext context)
+    {
+        if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
+        {
+            Output.Value = Background.Value;
+            return Output.Value;
+        }
+
+        blendPaint.Color = new Color(255, 255, 255, 255);
+        blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
+
+        VecI targetSize = GetTargetSize(context);
+        bool shouldClear = Background.Value == null;
+
+        if (FilterlessOutput.Connections.Count > 0)
+        {
+            var filterlessWorkingSurface = TryInitWorkingSurface(targetSize, context, 0);
+
+            if (Background.Value != null)
+            {
+                DrawBackground(filterlessWorkingSurface, context);
+                blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+            }
+
+            DrawLayer(context, filterlessWorkingSurface, shouldClear, useFilters: false);
+            blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
+
+            FilterlessOutput.Value = filterlessWorkingSurface;
+        }
+
+        var rendered = RenderImage(targetSize, context, shouldClear);
+        Output.Value = rendered;
+
+        return rendered;
+    }
+
+    private Texture RenderImage(VecI size, RenderingContext context, bool shouldClear)
+    {
+        if (Output.Connections.Count > 0)
+        {
+            var outputWorkingSurface = TryInitWorkingSurface(size, context, 1);
+
+            if (!HasOperations())
+            {
+                if (Background.Value != null)
+                {
+                    DrawBackground(outputWorkingSurface, context);
+                    blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+                }
+
+                DrawLayer(context, outputWorkingSurface, shouldClear);
+
+                return outputWorkingSurface;
+            }
+
+            DrawLayer(context, outputWorkingSurface, true);
+
+            // shit gets downhill with mask on big canvases, TODO: optimize
+            ApplyMaskIfPresent(outputWorkingSurface, context);
+
+            if (Background.Value != null)
+            {
+                Texture tempSurface = RequestTexture(4, outputWorkingSurface.Size, true);
+                DrawBackground(tempSurface, context);
+                ApplyRasterClip(outputWorkingSurface, tempSurface);
+                blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+                tempSurface.DrawingSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0,
+                    blendPaint);
+
+                return tempSurface;
+            }
+
+            return outputWorkingSurface;
+        }
+
+        return null;
+    }
+
+    protected abstract VecI GetTargetSize(RenderingContext ctx);
+
+    protected virtual void DrawLayer(RenderingContext ctx, Texture workingSurface, bool shouldClear,
+        bool useFilters = true)
+    {
+        blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255));
+
+        if (useFilters && Filters.Value != null)
+        {
+            blendPaint.SetFilters(Filters.Value);
+            DrawWithFilters(ctx, workingSurface, shouldClear, blendPaint);
+        }
+        else
+        {
+            blendPaint.SetFilters(null);
+            DrawWithoutFilters(ctx, workingSurface, shouldClear, blendPaint);
+        }
+    }
+    
+    protected abstract void DrawWithoutFilters(RenderingContext ctx, Texture workingSurface, bool shouldClear,
+        Paint paint);
+    
+    protected abstract void DrawWithFilters(RenderingContext ctx, Texture workingSurface, bool shouldClear,
+        Paint paint);
+
+    protected Texture TryInitWorkingSurface(VecI imageSize, RenderingContext context, int id)
+    {
+        ChunkResolution targetResolution = context.ChunkResolution;
+        bool hasSurface = workingSurfaces.TryGetValue((targetResolution, id), out Texture workingSurface);
+        VecI targetSize = (VecI)(imageSize * targetResolution.Multiplier());
+        
+        targetSize = new VecI(Math.Max(1, targetSize.X), Math.Max(1, targetSize.Y));
+
+        if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
+        {
+            workingSurfaces[(targetResolution, id)] = new Texture(targetSize);
+            workingSurface = workingSurfaces[(targetResolution, id)];
+        }
+
+        return workingSurface;
+    }
+
+    public abstract bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame);
 }

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

@@ -352,6 +352,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public Node Clone()
     {
         var clone = CreateCopy();
+        clone.DisplayName = DisplayName;
         clone.Id = Guid.NewGuid();
         clone.Position = Position;
 
@@ -420,7 +421,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
     }
 
-    internal virtual void DeserializeData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
+    internal virtual OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
     {
+        return new None();
     }
 }

+ 0 - 55
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs

@@ -1,55 +0,0 @@
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
-
-public class EllipseData : ShapeData
-{
-    public VecD Center { get; set; }
-    public VecD Radius { get; set; }
-
-    public EllipseData(VecD center, VecD radius)
-    {
-        Center = center;
-        Radius = radius;
-    }
-    
-    public override void Rasterize(DrawingSurface drawingSurface)
-    {
-        var imageSize = new VecI((int)Radius.X * 2, (int)Radius.Y * 2);
-
-        using ChunkyImage img = new ChunkyImage(imageSize);
-        RectI rect = new RectI(0, 0, (int)Radius.X * 2, (int)Radius.Y * 2);
-        
-        img.EnqueueDrawEllipse(rect, StrokeColor, FillColor, StrokeWidth);
-        img.CommitChanges();
-        
-        VecI pos = new VecI((int)(Center.X - Radius.X), (int)(Center.Y - Radius.Y));
-        img.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, drawingSurface, pos);
-    }
-
-    public override bool IsValid()
-    {
-        return Radius is { X: > 0, Y: > 0 };
-    }
-
-    public override int CalculateHash()
-    {
-        return HashCode.Combine(Center, Radius);
-    }
-
-    public override int GetCacheHash()
-    {
-        return CalculateHash();
-    }
-
-    public override object Clone()
-    {
-        return new EllipseData(Center, Radius)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth
-        };
-    }
-}

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

@@ -0,0 +1,102 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
+{
+    public VecD Radius { get; set; }
+    public VecD Center { get; set; }
+    
+    public override RectD GeometryAABB =>
+        new ShapeCorners(Center, Radius * 2).AABBBounds;
+
+    public override ShapeCorners TransformationCorners =>
+        new ShapeCorners(Center, Radius * 2).WithMatrix(TransformationMatrix);
+
+
+    public EllipseVectorData(VecD center, VecD radius)
+    {
+        Center = center;
+        Radius = radius;
+    }
+
+    public override void RasterizeGeometry(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, false);
+    }
+
+    public override void RasterizeTransformed(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, true);
+    }
+
+    private void Rasterize(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint, bool applyTransform)
+    {
+        var imageSize = (VecI)(Radius * 2);
+        
+        using ChunkyImage img = new ChunkyImage((VecI)GeometryAABB.Size);
+
+        RectD rotated = new ShapeCorners(RectD.FromTwoPoints(VecD.Zero, imageSize)).AABBBounds;
+
+        VecI shift = new VecI((int)Math.Floor(-rotated.Left), (int)Math.Floor(-rotated.Top));
+        RectI drawRect = new(shift, imageSize);
+        
+        img.EnqueueDrawEllipse(drawRect, StrokeColor, FillColor, StrokeWidth);
+        img.CommitChanges();
+
+        VecI topLeft = new VecI((int)Math.Round(Center.X - Radius.X), (int)Math.Round(Center.Y - Radius.Y)) - shift;
+        topLeft = (VecI)(topLeft * resolution.Multiplier());
+        
+        RectI region = new(VecI.Zero, (VecI)GeometryAABB.Size);
+
+        int num = 0;
+        if (applyTransform)
+        {
+            num = drawingSurface.Canvas.Save();
+            Matrix3X3 final = TransformationMatrix with
+            {
+                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
+                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
+            };
+            drawingSurface.Canvas.SetMatrix(final);
+        }
+
+        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
+
+        if (applyTransform)
+        {
+            drawingSurface.Canvas.RestoreToCount(num);
+        }
+    }
+
+    public override bool IsValid()
+    {
+        return Radius is { X: > 0, Y: > 0 };
+    }
+
+    public override int CalculateHash()
+    {
+        return HashCode.Combine(Center, Radius, StrokeColor, FillColor, StrokeWidth,  TransformationMatrix);
+    }
+
+    public override int GetCacheHash()
+    {
+        return CalculateHash();
+    }
+
+    public override object Clone()
+    {
+        return new EllipseVectorData(Center, Radius)
+        {
+            StrokeColor = StrokeColor,
+            FillColor = FillColor,
+            StrokeWidth = StrokeWidth,
+            TransformationMatrix = TransformationMatrix
+        };
+    }
+}

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

@@ -0,0 +1,132 @@
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnlyLineData
+{
+    public VecD Start { get; set; } = startPos;
+    public VecD End { get; set; } = pos;
+
+    public VecD TransformedStart
+    {
+        get => TransformationMatrix.MapPoint(Start);
+        set => Start = TransformationMatrix.Invert().MapPoint(value);
+    }
+
+    public VecD TransformedEnd
+    {
+        get => TransformationMatrix.MapPoint(End);
+        set => End = TransformationMatrix.Invert().MapPoint(value);
+    }
+
+    public override RectD GeometryAABB
+    {
+        get
+        {
+            if (StrokeWidth == 1)
+            {
+                return RectD.FromTwoPoints(Start, End);
+            }
+            
+            VecD halfStroke = new(StrokeWidth / 2f);
+            VecD min = new VecD(Math.Min(Start.X, End.X), Math.Min(Start.Y, End.Y)) - halfStroke;
+            VecD max = new VecD(Math.Max(Start.X, End.X), Math.Max(Start.Y, End.Y)) + halfStroke;
+            
+            return new RectD(min, max - min);
+        }
+    }
+
+    public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
+        .WithMatrix(TransformationMatrix);
+
+    public override void RasterizeGeometry(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, false);
+    }
+
+    public override void RasterizeTransformed(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, true);
+    }
+
+    private void Rasterize(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint, bool applyTransform)
+    {
+        RectD adjustedAABB = GeometryAABB.RoundOutwards();
+        adjustedAABB = adjustedAABB with { Size = adjustedAABB.Size + new VecD(1, 1) };
+        var imageSize = (VecI)adjustedAABB.Size;
+        
+        using ChunkyImage img = new ChunkyImage(imageSize);
+
+        if (StrokeWidth == 1)
+        {
+            VecD adjustment = new VecD(0.5, 0.5); 
+            
+            img.EnqueueDrawBresenhamLine(
+                (VecI)(Start - adjustedAABB.TopLeft - adjustment),
+                (VecI)(End - adjustedAABB.TopLeft - adjustment), StrokeColor, BlendMode.SrcOver); 
+        }
+        else
+        {
+            img.EnqueueDrawSkiaLine(
+                (VecI)Start.Round() - (VecI)adjustedAABB.TopLeft,
+                (VecI)End.Round() - (VecI)adjustedAABB.TopLeft, StrokeCap.Butt, StrokeWidth, StrokeColor, BlendMode.SrcOver);
+        }
+
+        img.CommitChanges();
+        
+        VecI topLeft = (VecI)(adjustedAABB.TopLeft * resolution.Multiplier()); 
+        
+        RectI region = new(VecI.Zero, imageSize);
+        
+        int num = 0;
+
+        if (applyTransform)
+        {
+            num = drawingSurface.Canvas.Save();
+            Matrix3X3 final = TransformationMatrix with
+            {
+                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
+                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
+            };
+            drawingSurface.Canvas.SetMatrix(final);
+        }
+
+        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
+        
+        if (applyTransform)
+        {
+            drawingSurface.Canvas.RestoreToCount(num);
+        }
+    }
+
+    public override bool IsValid()
+    {
+        return Start != End;
+    }
+
+    public override int GetCacheHash()
+    {
+        return HashCode.Combine(Start, End, StrokeColor, StrokeWidth, TransformationMatrix);
+    }
+
+    public override int CalculateHash()
+    {
+        return GetCacheHash();
+    }
+
+    public override object Clone()
+    {
+        return new LineVectorData(Start, End)
+        {
+            StrokeColor = StrokeColor,
+            StrokeWidth = StrokeWidth,
+            TransformationMatrix = TransformationMatrix
+        };
+    }
+}

+ 0 - 49
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsData.cs

@@ -1,49 +0,0 @@
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
-
-public class PointsData : ShapeData
-{
-    public List<VecD> Points { get; set; } = new();
-    
-    public PointsData(IEnumerable<VecD> points)
-    {
-        Points = new List<VecD>(points);
-    }
-    
-    public override void Rasterize(DrawingSurface drawingSurface)
-    {
-        using Paint paint = new Paint();
-        paint.Color = FillColor;
-        paint.StrokeWidth = StrokeWidth;
-        
-        drawingSurface.Canvas.DrawPoints(PointMode.Points, Points.Select(p => new Point((int)p.X, (int)p.Y)).ToArray(), paint);
-    }
-
-    public override bool IsValid()
-    {
-        return Points.Count > 0;
-    }
-
-    public override int GetCacheHash()
-    {
-        return CalculateHash();
-    }
-
-    public override int CalculateHash()
-    {
-        return Points.GetHashCode();
-    }
-
-    public override object Clone()
-    {
-        return new PointsData(Points)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth
-        };
-    }
-}

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

@@ -0,0 +1,81 @@
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class PointsVectorData : ShapeVectorData
+{
+    public List<VecD> Points { get; set; } = new();
+
+    public PointsVectorData(IEnumerable<VecD> points)
+    {
+        Points = new List<VecD>(points);
+    }
+
+    public override RectD GeometryAABB => new RectD(Points.Min(p => p.X), Points.Min(p => p.Y), Points.Max(p => p.X),
+        Points.Max(p => p.Y));
+
+    public override ShapeCorners TransformationCorners => new ShapeCorners(
+        GeometryAABB).WithMatrix(TransformationMatrix);
+
+    public override void RasterizeGeometry(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint)
+    {
+        Rasterize(drawingSurface, paint, resolution, false);
+    }
+
+    public override void RasterizeTransformed(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint)
+    {
+        Rasterize(drawingSurface, paint, resolution, true);
+    }
+
+    private void Rasterize(DrawingSurface drawingSurface, Paint paint, ChunkResolution resolution, bool applyTransform)
+    {
+        paint.Color = FillColor;
+        paint.StrokeWidth = StrokeWidth;
+
+        int num = 0;
+        if (applyTransform)
+        {
+            num = drawingSurface.Canvas.Save();
+            Matrix3X3 final = TransformationMatrix with
+            {
+                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
+                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
+            };
+            drawingSurface.Canvas.SetMatrix(final);
+        }
+
+        drawingSurface.Canvas.DrawPoints(PointMode.Points, Points.Select(p => new Point((int)p.X, (int)p.Y)).ToArray(),
+            paint);
+
+        if (applyTransform)
+        {
+            drawingSurface.Canvas.RestoreToCount(num);
+        }
+    }
+
+    public override bool IsValid()
+    {
+        return Points.Count > 0;
+    }
+
+    public override int GetCacheHash()
+    {
+        return CalculateHash();
+    }
+
+    public override int CalculateHash()
+    {
+        return Points.GetHashCode();
+    }
+
+    public override object Clone()
+    {
+        return new PointsVectorData(Points)
+        {
+            StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
+        };
+    }
+}

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

@@ -0,0 +1,98 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
+{
+    public VecD Center { get; }
+    public VecD Size { get; }
+
+    public override RectD GeometryAABB => RectD.FromCenterAndSize(Center, Size); 
+
+    public override ShapeCorners TransformationCorners =>
+        new ShapeCorners(Center, Size).WithMatrix(TransformationMatrix);
+
+
+    public RectangleVectorData(VecD center, VecD size)
+    {
+        Center = center;
+        Size = size;
+    }
+
+    public override void RasterizeGeometry(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, false);
+    }
+
+    public override void RasterizeTransformed(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint)
+    {
+        Rasterize(drawingSurface, resolution, paint, true);
+    }
+
+    private void Rasterize(DrawingSurface drawingSurface, ChunkResolution resolution, Paint paint, bool applyTransform)
+    {
+        var imageSize = (VecI)Size; 
+
+        using ChunkyImage img = new ChunkyImage(imageSize);
+
+        RectI drawRect = (RectI)RectD.FromTwoPoints(VecD.Zero, Size).RoundOutwards();
+
+        ShapeData data = new ShapeData(drawRect.Center, drawRect.Size, 0, StrokeWidth, StrokeColor, FillColor);
+        img.EnqueueDrawRectangle(data);
+        img.CommitChanges();
+        
+        VecI topLeft = (VecI)((Center - Size / 2) * resolution.Multiplier());
+        RectI region = new(VecI.Zero, (VecI)GeometryAABB.Size);
+
+        int num = 0;
+        if (applyTransform)
+        {
+            num = drawingSurface.Canvas.Save();
+            Matrix3X3 final = TransformationMatrix with
+            {
+                TransX = TransformationMatrix.TransX * (float)resolution.Multiplier(),
+                TransY = TransformationMatrix.TransY * (float)resolution.Multiplier()
+            };
+            drawingSurface.Canvas.SetMatrix(final);
+        }
+
+        img.DrawMostUpToDateRegionOn(region, resolution, drawingSurface, topLeft, paint);
+        
+        if (applyTransform)
+        {
+            drawingSurface.Canvas.RestoreToCount(num);
+        }
+    }
+
+    public override bool IsValid()
+    {
+        return Size is { X: > 0, Y: > 0 };
+    }
+
+    public override int CalculateHash()
+    {
+        return HashCode.Combine(Center, Size, StrokeColor, FillColor, StrokeWidth, TransformationMatrix);
+    }
+
+    public override int GetCacheHash()
+    {
+        return CalculateHash();
+    }
+
+    public override object Clone()
+    {
+        return new RectangleVectorData(Center, Size)
+        {
+            StrokeColor = StrokeColor,
+            FillColor = FillColor,
+            StrokeWidth = StrokeWidth,
+            TransformationMatrix = TransformationMatrix
+        };
+    }
+}

+ 0 - 24
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeData.cs

@@ -1,24 +0,0 @@
-using PixiEditor.Common;
-using PixiEditor.DrawingApi.Core.ColorsImpl;
-using PixiEditor.DrawingApi.Core.Surfaces;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
-
-public abstract class ShapeData : ICacheable, ICloneable
-{
-    public Color StrokeColor { get; set; } = Colors.White;
-    public Color FillColor { get; set; } = Colors.White;
-    public int StrokeWidth { get; set; } = 1;
-
-    public abstract void Rasterize(DrawingSurface drawingSurface);
-    public abstract bool IsValid();
-
-    public abstract int GetCacheHash();
-    public abstract int CalculateHash();
-    public abstract object Clone();
-
-    public override int GetHashCode()
-    {
-        return CalculateHash();
-    }
-}

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

@@ -0,0 +1,33 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Common;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVectorData
+{
+    public Matrix3X3 TransformationMatrix { get; set; } = Matrix3X3.Identity; 
+    
+    public Color StrokeColor { get; set; } = Colors.White;
+    public Color FillColor { get; set; } = Colors.White;
+    public int StrokeWidth { get; set; } = 1;
+    public abstract RectD GeometryAABB { get; }
+    public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
+    public abstract ShapeCorners TransformationCorners { get; } 
+
+    public abstract void RasterizeGeometry(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint);
+    public abstract void RasterizeTransformed(DrawingSurface drawingSurface, ChunkResolution resolution, Paint? paint);
+    public abstract bool IsValid();
+    public abstract int GetCacheHash();
+    public abstract int CalculateHash();
+    public abstract object Clone();
+
+    public override int GetHashCode()
+    {
+        return CalculateHash();
+    }
+}

+ 4 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs

@@ -1,12 +1,11 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("DistributePoints")]
-public class DistributePointsNode : ShapeNode<PointsData>
+public class DistributePointsNode : ShapeNode<PointsVectorData>
 {
     public InputProperty<int> MaxPointCount { get; }
 
@@ -19,12 +18,12 @@ public class DistributePointsNode : ShapeNode<PointsData>
         Seed = CreateInput("Seed", "SEED", 0);
     }
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
         return GetPointsRandomly(context.DocumentSize);
     }
 
-    private PointsData GetPointsRandomly(VecI size)
+    private PointsVectorData GetPointsRandomly(VecI size)
     {
         var seed = Seed.Value;
         var random = new Random(seed);
@@ -36,7 +35,7 @@ public class DistributePointsNode : ShapeNode<PointsData>
             finalPoints.Add(new VecD(random.NextDouble() * size.X, random.NextDouble() * size.Y));
         }
         
-        var shapeData = new PointsData(finalPoints);
+        var shapeData = new PointsVectorData(finalPoints);
         return shapeData;
     }
 

+ 3 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs

@@ -2,12 +2,11 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("Ellipse")]
-public class EllipseNode : ShapeNode<EllipseData>
+public class EllipseNode : ShapeNode<EllipseVectorData>
 {
     public InputProperty<VecD> Position { get; }
     public InputProperty<VecD> Radius { get; }
@@ -25,9 +24,9 @@ public class EllipseNode : ShapeNode<EllipseData>
         StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
     }
 
-    protected override EllipseData? GetShapeData(RenderingContext context)
+    protected override EllipseVectorData? GetShapeData(RenderingContext context)
     {
-        return new EllipseData(Position.Value, Radius.Value)
+        return new EllipseVectorData(Position.Value, Radius.Value)
             { StrokeColor = StrokeColor.Value, FillColor = FillColor.Value, StrokeWidth = StrokeWidth.Value };
     }
 

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

@@ -1,10 +1,10 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
@@ -13,13 +13,16 @@ public class RasterizeShapeNode : Node
 {
     public OutputProperty<Texture> Image { get; }
 
-    public InputProperty<ShapeData> Data { get; }
+    public InputProperty<ShapeVectorData> Data { get; }
+
+
+    protected override bool AffectedByChunkResolution => true;
 
 
     public RasterizeShapeNode()
     {
         Image = CreateOutput<Texture>("Image", "IMAGE", null);
-        Data = CreateInput<ShapeData>("Points", "SHAPE", null);
+        Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
     }
 
     protected override Texture? OnExecute(RenderingContext context)
@@ -32,7 +35,7 @@ public class RasterizeShapeNode : Node
         var size = context.DocumentSize;
         var image = RequestTexture(0, size);
         
-        shape.Rasterize(image.DrawingSurface);
+        shape.RasterizeTransformed(image.DrawingSurface, context.ChunkResolution, null);
 
         Image.Value = image;
         

+ 5 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -2,14 +2,13 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("RemoveClosePoints")]
-public class RemoveClosePointsNode : ShapeNode<PointsData>
+public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 {
-    public InputProperty<PointsData> Input { get; }
+    public InputProperty<PointsVectorData> Input { get; }
 
     public InputProperty<double> MinDistance { get; }
 
@@ -17,12 +16,12 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
 
     public RemoveClosePointsNode()
     {
-        Input = CreateInput<PointsData>("Input", "POINTS", null);
+        Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
         MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
         Seed = CreateInput("Seed", "SEED", 0);
     }
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
         var data = Input.Value;
 
@@ -64,7 +63,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
             newPoints.Add(availablePoints[0]);
         }
 
-        var finalData = new PointsData(newPoints);
+        var finalData = new PointsVectorData(newPoints);
 
         return finalData;
     }

+ 6 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs

@@ -1,11 +1,12 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-public abstract class ShapeNode<T> : Node where T : ShapeData
+public abstract class ShapeNode<T> : Node where T : ShapeVectorData
 {
     public OutputProperty<T> Output { get; }
     
@@ -28,11 +29,11 @@ public abstract class ShapeNode<T> : Node where T : ShapeData
     
     protected abstract T? GetShapeData(RenderingContext context);
 
-    public Texture RasterizePreview(ShapeData data, VecI size)
+    public Texture RasterizePreview(ShapeVectorData vectorData, VecI size)
     {
         Texture texture = RequestTexture(0, size);
         
-        data.Rasterize(texture.DrawingSurface);
+        vectorData.RasterizeTransformed(texture.DrawingSurface, ChunkResolution.Full, null);
         
         return texture;
     }

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

@@ -1,6 +1,8 @@
 using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -27,6 +29,10 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     public OutputProperty<Texture?> FilterlessOutput { get; }
 
     public ChunkyImage? EmbeddedMask { get; set; }
+    public virtual ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return new ShapeCorners(GetTightBounds(frameTime).GetValueOrDefault());
+    }
 
     public string MemberName
     {
@@ -144,6 +150,9 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         int width = (int)(chunkSize);
         int height = (int)(chunkSize);
 
+        x = Math.Clamp(x, 0, Math.Max(sourceSize.X - width, 0));
+        y = Math.Clamp(y, 0, Math.Max(sourceSize.Y - height, 0));
+        
         return new RectI(x, y, width, height);
     }
 
@@ -160,7 +169,34 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         return new RectI(x, y, width, height);
     }
 
-    public abstract RectI? GetTightBounds(KeyFrameTime frameTime);
+    public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
+
+
+    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    {
+        base.SerializeAdditionalData(additionalData);
+        if (EmbeddedMask != null)
+        {
+            additionalData["embeddedMask"] = EmbeddedMask;
+        }
+    }
+
+    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
+    {
+        base.DeserializeAdditionalData(target, data);
+        bool hasMask = data.ContainsKey("embeddedMask");
+        if (hasMask)
+        {
+            ChunkyImage? mask = (ChunkyImage?)data["embeddedMask"];
+            
+            EmbeddedMask?.Dispose();
+            EmbeddedMask = mask; 
+            
+            return new List<IChangeInfo> { new StructureMemberMask_ChangeInfo(Id, mask != null) };
+        }
+
+        return new None();
+    }
 
     public override void Dispose()
     {

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

@@ -0,0 +1,167 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("VectorLayer")]
+public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable
+{
+    public Matrix3X3 TransformationMatrix
+    {
+        get => ShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
+        set
+        {
+            if (ShapeData == null)
+            {
+                return;
+            }
+
+            ShapeData.TransformationMatrix = value;
+        }
+    }
+
+    public ShapeVectorData? ShapeData { get; set; }
+    IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
+
+    protected override bool AffectedByChunkResolution => true;
+
+    private int lastCacheHash;
+
+    protected override Texture? OnExecute(RenderingContext context)
+    {
+        var rendered = base.OnExecute(context);
+
+        Output.Value = rendered;
+
+        return rendered;
+    }
+
+    protected override VecI GetTargetSize(RenderingContext ctx)
+    {
+        return ctx.DocumentSize;
+    }
+
+    protected override void DrawWithoutFilters(RenderingContext ctx, Texture workingSurface, bool shouldClear,
+        Paint paint)
+    {
+        if (ShapeData == null)
+        {
+            return;
+        }
+
+        if (shouldClear)
+        {
+            workingSurface.DrawingSurface.Canvas.Clear();
+        }
+
+        Rasterize(workingSurface.DrawingSurface, ctx.ChunkResolution, paint);
+    }
+
+    protected override void DrawWithFilters(RenderingContext ctx, Texture workingSurface, bool shouldClear, Paint paint)
+    {
+        if (ShapeData == null)
+        {
+            return;
+        }
+
+        if (shouldClear)
+        {
+            workingSurface.DrawingSurface.Canvas.Clear();
+        }
+
+        Rasterize(workingSurface.DrawingSurface, ctx.ChunkResolution, paint);
+    }
+
+    public override bool RenderPreview(Texture renderOn, VecI chunk, ChunkResolution resolution, int frame)
+    {
+        if (ShapeData == null)
+        {
+            return false;
+        }
+
+        using var paint = new Paint();
+
+        VecI tightBoundsSize = (VecI)ShapeData.TransformedAABB.Size;
+
+        VecI translation = new VecI(
+            (int)Math.Max(ShapeData.TransformedAABB.TopLeft.X, 0),
+            (int)Math.Max(ShapeData.TransformedAABB.TopLeft.Y, 0));
+        
+        VecI size = tightBoundsSize + translation;
+        
+        if (size.X == 0 || size.Y == 0)
+        {
+            return false;
+        }
+
+        using Texture toRasterizeOn = new(size);
+
+        int save = toRasterizeOn.DrawingSurface.Canvas.Save();
+
+        Matrix3X3 matrix = ShapeData.TransformationMatrix;
+        Rasterize(toRasterizeOn.DrawingSurface, resolution, paint);
+
+        renderOn.DrawingSurface.Canvas.DrawSurface(toRasterizeOn.DrawingSurface, 0, 0, paint);
+
+        toRasterizeOn.DrawingSurface.Canvas.RestoreToCount(save);
+        return true;
+    }
+
+    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    {
+        base.SerializeAdditionalData(additionalData);
+        additionalData["ShapeData"] = ShapeData;
+    }
+
+    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target,
+        IReadOnlyDictionary<string, object> data)
+    {
+        base.DeserializeAdditionalData(target, data);
+        ShapeData = (ShapeVectorData)data["ShapeData"];
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)ShapeData.TransformedAABB, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(Id, affected);
+    }
+
+    protected override bool CacheChanged(RenderingContext context)
+    {
+        return base.CacheChanged(context) || (ShapeData?.GetCacheHash() ?? -1) != lastCacheHash;
+    }
+
+    protected override void UpdateCache(RenderingContext context)
+    {
+        base.UpdateCache(context);
+        lastCacheHash = ShapeData?.GetCacheHash() ?? -1;
+    }
+
+    public override RectD? GetTightBounds(KeyFrameTime frameTime)
+    {
+        return ShapeData?.TransformedAABB ?? null;
+    }
+
+    public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return ShapeData?.TransformationCorners ?? new ShapeCorners();
+    }
+
+    public void Rasterize(DrawingSurface surface, ChunkResolution resolution, Paint paint)
+    {
+        ShapeData?.RasterizeTransformed(surface, resolution, paint);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), };
+    }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IRasterizable.cs

@@ -0,0 +1,9 @@
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface IRasterizable
+{
+    public void Rasterize(DrawingSurface surface, ChunkResolution resolution, Paint paint);
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -31,7 +31,7 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         this.frame = frame;
         // TODO: pos is unused, check if it should be added to positions
         
-        ellipseLines = EllipseHelper.SplitEllipseIntoLines((EllipseHelper.GenerateEllipseFromRect(new RectI(0, 0, strokeWidth, strokeWidth))));
+        ellipseLines = EllipseHelper.SplitEllipseIntoLines((EllipseHelper.GenerateEllipseFromRect(new RectI(0, 0, strokeWidth, strokeWidth), 0)));
     }
 
     [UpdateChangeMethod]

+ 49 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -1,4 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -15,6 +17,7 @@ internal class CombineStructureMembersOnto_Change : Change
 
     private Guid targetLayer;
     private CommittedChunkStorage? originalChunks;
+    
 
     [GenerateMakeChangeAction]
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer, int frame)
@@ -62,24 +65,36 @@ internal class CombineStructureMembersOnto_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
     {
-        //TODO: Add support for different Layer types
-        var toDrawOn = target.FindMemberOrThrow<ImageLayerNode>(targetLayer);
+        // TODO: add merging similar layers (vector -> vector)
+        var toDrawOn = target.FindMemberOrThrow<LayerNode>(targetLayer);
 
         var chunksToCombine = new HashSet<VecI>();
         foreach (var guid in layersToCombine)
         {
-            var layer = target.FindMemberOrThrow<ImageLayerNode>(guid);
-            var layerImage = layer.GetLayerImageAtFrame(frame);
-            chunksToCombine.UnionWith(layerImage.FindAllChunks());
-        }
+            var layer = target.FindMemberOrThrow<LayerNode>(guid);
+            if(layer is not IRasterizable or ImageLayerNode)
+                continue;
 
-        var toDrawOnImage = toDrawOn.GetLayerImageAtFrame(frame);
+            if (layer is ImageLayerNode imageLayerNode)
+            {
+                var layerImage = imageLayerNode.GetLayerImageAtFrame(frame);
+                chunksToCombine.UnionWith(layerImage.FindAllChunks());
+            }
+            else
+            {
+                AddChunksByTightBounds(layer, chunksToCombine);
+            }
+        }
+        
+        List<IChangeInfo> changes = new();
+        
+        var toDrawOnImage = ((ImageLayerNode)toDrawOn).GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
 
         DocumentRenderer renderer = new(target);
 
         AffectedArea affArea = new();
-        DrawingBackendApi.Current.RenderingServer.Invoke(() =>
+        DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
             RectI? globalClippingRect = new RectI(0, 0, target.Size.X, target.Size.Y);
             foreach (var chunk in chunksToCombine)
@@ -100,7 +115,27 @@ internal class CombineStructureMembersOnto_Change : Change
 
 
         ignoreInUndo = false;
-        return new LayerImageArea_ChangeInfo(targetLayer, affArea);
+        
+        changes.Add(new LayerImageArea_ChangeInfo(targetLayer, affArea));
+        return changes;
+    }
+
+    private void AddChunksByTightBounds(LayerNode layer, HashSet<VecI> chunksToCombine)
+    {
+        var tightBounds = layer.GetTightBounds(frame);
+        if (tightBounds.HasValue)
+        {
+            VecI chunk = (VecI)tightBounds.Value.TopLeft / ChunkyImage.FullChunkSize;
+            VecI sizeInChunks = ((VecI)tightBounds.Value.Size / ChunkyImage.FullChunkSize);
+            sizeInChunks = new VecI(Math.Max(1, sizeInChunks.X), Math.Max(1, sizeInChunks.Y));
+            for (int x = 0; x < sizeInChunks.X; x++)
+            {
+                for (int y = 0; y < sizeInChunks.Y; y++)
+                {
+                    chunksToCombine.Add(chunk + new VecI(x, y));
+                }
+            }
+        }
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
@@ -109,7 +144,11 @@ internal class CombineStructureMembersOnto_Change : Change
         var affectedArea =
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(toDrawOn.GetLayerImageAtFrame(frame),
                 ref originalChunks);
-        return new LayerImageArea_ChangeInfo(targetLayer, affectedArea);
+        
+        List<IChangeInfo> changes = new();
+        changes.Add(new LayerImageArea_ChangeInfo(targetLayer, affectedArea));
+
+        return changes; 
     }
 
     public override void Dispose()

+ 13 - 7
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawEllipse_UpdateableChange.cs → src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterEllipse_UpdateableChange.cs

@@ -3,23 +3,25 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-internal class DrawEllipse_UpdateableChange : UpdateableChange
+internal class DrawRasterEllipse_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
     private RectI location;
-    private readonly Color strokeColor;
-    private readonly Color fillColor;
-    private readonly int strokeWidth;
+    private double rotation;
+    private Color strokeColor;
+    private Color fillColor;
+    private int strokeWidth;
     private readonly bool drawOnMask;
     private int frame;
 
     private CommittedChunkStorage? storedChunks;
 
     [GenerateUpdateableChangeActions]
-    public DrawEllipse_UpdateableChange(Guid memberGuid, RectI location, Color strokeColor, Color fillColor, int strokeWidth, bool drawOnMask, int frame)
+    public DrawRasterEllipse_UpdateableChange(Guid memberGuid, RectI location, double rotationRad, Color strokeColor, Color fillColor, int strokeWidth, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.location = location;
+        this.rotation = rotationRad;
         this.strokeColor = strokeColor;
         this.fillColor = fillColor;
         this.strokeWidth = strokeWidth;
@@ -28,9 +30,13 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
     }
 
     [UpdateChangeMethod]
-    public void Update(RectI location)
+    public void Update(RectI location, double rotationRad, Color strokeColor, Color fillColor, int strokeWidth)
     {
         this.location = location;
+        rotation = rotationRad;
+        this.strokeColor = strokeColor;
+        this.fillColor = fillColor;
+        this.strokeWidth = strokeWidth;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -47,7 +53,7 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
         if (!location.IsZeroOrNegativeArea)
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
-            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth);
+            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth, rotation);
         }
 
         var affectedArea = targetImage.FindAffectedArea();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawLine_UpdateableChange.cs → src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterLine_UpdateableChange.cs

@@ -5,7 +5,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
-internal class DrawLine_UpdateableChange : UpdateableChange
+internal class DrawRasterLine_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
     private VecI from;
@@ -18,7 +18,7 @@ internal class DrawLine_UpdateableChange : UpdateableChange
     private int frame;
 
     [GenerateUpdateableChangeActions]
-    public DrawLine_UpdateableChange
+    public DrawRasterLine_UpdateableChange
         (Guid memberGuid, VecI from, VecI to, int strokeWidth, Color color, StrokeCap caps, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs → src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRasterRectangle_UpdateableChange.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
-internal class DrawRectangle_UpdateableChange : UpdateableChange
+internal class DrawRasterRectangle_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
     private ShapeData rect;
@@ -11,7 +11,7 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
     private int frame;
     
     [GenerateUpdateableChangeActions]
-    public DrawRectangle_UpdateableChange(Guid memberGuid, ShapeData rectangle, bool drawOnMask, int frame)
+    public DrawRasterRectangle_UpdateableChange(Guid memberGuid, ShapeData rectangle, bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.rect = rectangle;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -111,7 +111,7 @@ internal static class DrawingChangeHelper
             // If it should draw on the mask, the mask can't be null
             true when member.EmbeddedMask is null => false,
             // If it should not draw on the mask, the member can't be a folder
-            false when member is FolderNode => false,
+            false when member is not ImageLayerNode => false,
             _ => true
         };
     }

+ 15 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -1,4 +1,5 @@
-using PixiEditor.DrawingApi.Core.ColorsImpl;
+using ChunkyImageLib.Operations;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -63,10 +64,19 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         {
             image.EnqueueDrawBresenhamLine(from, to, color, BlendMode.Src);
         }
+        else if (strokeWidth <= 10)
+        {
+            var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
+            foreach (var point in bresenham)
+            {
+                var rect = new RectI(point - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+                image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
+            }
+        }
         else
         {
             var rect = new RectI(to - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-            image.EnqueueDrawEllipse(rect, color, color, 1, srcPaint);
+            image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
             image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
         }
         var affChunks = image.FindAffectedArea(opCount);
@@ -85,13 +95,13 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             else
             {
                 var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, srcPaint);
+                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
             }
             return;
         }
 
         var firstRect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-        targetImage.EnqueueDrawEllipse(firstRect, color, color, 1, srcPaint);
+        targetImage.EnqueueDrawEllipse(firstRect, color, color, 1, 0, srcPaint);
 
         for (int i = 1; i < points.Count; i++)
         {
@@ -102,7 +112,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             else
             {
                 var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, srcPaint);
+                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
                 targetImage.EnqueueDrawSkiaLine(points[i - 1], points[i], StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
             }
         }

+ 31 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs

@@ -4,13 +4,14 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal class ShiftLayer_UpdateableChange : UpdateableChange
 {
     private List<Guid> layerGuids;
     private bool keepOriginal;
     private VecI delta;
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
-    
+
     private List<IChangeInfo> _tempChanges = new();
     private int frame;
 
@@ -36,7 +37,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         {
             if (!target.HasMember(layer)) return false;
         }
-        
+
         return true;
     }
 
@@ -47,18 +48,26 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         this.keepOriginal = keepOriginal;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage?>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            // TODO: This now does't crash, but ignores other layer types. Think how to handle this.
+            if (layer is not ImageLayerNode)
+            {
+                continue;
+            }
+
             var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            // TODO: Add support for different Layer types
             var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-            
+
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
-            
+
             originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
             image.CommitChanges();
         }
@@ -73,10 +82,17 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
 
         foreach (var layerGuid in layerGuids)
         {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is not ImageLayerNode)
+            {
+                continue;
+            }
+
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
             _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
-        
+
         return _tempChanges;
     }
 
@@ -85,12 +101,19 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is not ImageLayerNode)
+            {
+                continue;
+            }
+
             var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
-        
+
         return changes;
     }
 

+ 0 - 202
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -1,202 +0,0 @@
-using ChunkyImageLib.Operations;
-using PixiEditor.ChangeableDocument.Changes.Selection;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.DrawingApi.Core.Surfaces.Vector;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-internal class TransformSelectedArea_UpdateableChange : UpdateableChange
-{
-    private readonly Guid[] membersToTransform;
-    private readonly bool drawOnMask;
-    private bool keepOriginal;
-    private ShapeCorners corners;
-
-    private Dictionary<Guid, (Surface surface, VecI pos)>? images;
-    private Matrix3X3 globalMatrix;
-    private Dictionary<Guid, CommittedChunkStorage>? savedChunks;
-
-    private RectD originalTightBounds;
-    private RectI roundedTightBounds;
-    private VectorPath? originalPath;
-
-    private bool hasEnqueudImages = false;
-    private int frame;
-
-    private static Paint RegularPaint { get; } = new () { BlendMode = BlendMode.SrcOver };
-
-    [GenerateUpdateableChangeActions]
-    public TransformSelectedArea_UpdateableChange(
-        IEnumerable<Guid> membersToTransform,
-        ShapeCorners corners,
-        bool keepOriginal,
-        bool transformMask,
-        int frame)
-    {
-        this.membersToTransform = membersToTransform.Select(static a => a).ToArray();
-        this.corners = corners;
-        this.keepOriginal = keepOriginal;
-        this.drawOnMask = transformMask;
-        this.frame = frame;
-    }
-
-    public override bool InitializeAndValidate(Document target)
-    {
-        if (membersToTransform.Length == 0 || target.Selection.SelectionPath.IsEmpty)
-            return false;
-
-        foreach (var guid in membersToTransform)
-        {
-            if (!DrawingChangeHelper.IsValidForDrawing(target, guid, drawOnMask))
-                return false;
-        }
-
-        originalPath = new VectorPath(target.Selection.SelectionPath) { FillType = PathFillType.EvenOdd };
-        
-        originalTightBounds = originalPath.TightBounds;
-        roundedTightBounds = (RectI)originalTightBounds.RoundOutwards();
-        //boundsRoundingOffset = bounds.TopLeft - roundedBounds.TopLeft;
-
-        images = new();
-        foreach (var guid in membersToTransform)
-        {
-            ChunkyImage image = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            var extracted = ExtractArea(image, originalPath, roundedTightBounds);
-            if (extracted.IsT0)
-                continue;
-            images.Add(guid, (extracted.AsT1.image, extracted.AsT1.extractedRect.Pos));
-        }
-
-        globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
-        return true;
-    }
-
-    public OneOf<None, (Surface image, RectI extractedRect)> ExtractArea(ChunkyImage image, VectorPath path, RectI pathBounds)
-    {
-        // get rid of transparent areas on edges
-        var memberImageBounds = image.FindChunkAlignedMostUpToDateBounds();
-        if (memberImageBounds is null)
-            return new None();
-        pathBounds = pathBounds.Intersect(memberImageBounds.Value);
-        pathBounds = pathBounds.Intersect(new RectI(VecI.Zero, image.LatestSize));
-        if (pathBounds.IsZeroOrNegativeArea)
-            return new None();
-
-        // shift the clip to account for the image being smaller than the selection
-        VectorPath clipPath = new VectorPath(path) { FillType = PathFillType.EvenOdd };
-        clipPath.Transform(Matrix3X3.CreateTranslation(-pathBounds.X, -pathBounds.Y));
-
-        // draw
-        Surface output = new(pathBounds.Size);
-        output.DrawingSurface.Canvas.Save();
-        output.DrawingSurface.Canvas.ClipPath(clipPath);
-        image.DrawMostUpToDateRegionOn(pathBounds, ChunkResolution.Full, output.DrawingSurface, VecI.Zero);
-        output.DrawingSurface.Canvas.Restore();
-
-        return (output, pathBounds);
-    }
-
-    [UpdateChangeMethod]
-    public void Update(ShapeCorners corners, bool keepOriginal)
-    {
-        this.keepOriginal = keepOriginal;
-        this.corners = corners;
-        globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
-    }
-
-    private AffectedArea DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
-    {
-        var prevAffArea = memberImage.FindAffectedArea();
-
-        memberImage.CancelChanges();
-
-        if (!keepOriginal)
-            memberImage.EnqueueClearPath(originalPath!, roundedTightBounds);
-        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - (float)originalTightBounds.Left, originalPos.Y - (float)originalTightBounds.Top);
-        localMatrix = localMatrix.PostConcat(globalMatrix);
-        memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
-        hasEnqueudImages = true;
-
-        var affectedArea = memberImage.FindAffectedArea();
-        affectedArea.UnionWith(prevAffArea);
-        return affectedArea;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
-    {
-        if (savedChunks is not null)
-            throw new InvalidOperationException("Apply called twice");
-        savedChunks = new();
-
-        List<IChangeInfo> infos = new();
-        foreach (var (guid, (image, pos)) in images!)
-        {
-            ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            var area = DrawImage(target, guid, image, pos, memberImage);
-            savedChunks[guid] = new(memberImage, memberImage.FindAffectedArea().Chunks);
-            memberImage.CommitChanges();
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, area, drawOnMask).AsT1);
-        }
-
-        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
-
-        hasEnqueudImages = false;
-        ignoreInUndo = false;
-        return infos;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        List<IChangeInfo> infos = new();
-        foreach (var (guid, (image, pos)) in images!)
-        {
-            ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
-        }
-        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
-        return infos;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
-    {
-        List<IChangeInfo> infos = new();
-        foreach (var (guid, storage) in savedChunks!)
-        {
-            var storageCopy = storage;
-            var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, frame, ref storageCopy);
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, chunks, drawOnMask).AsT1);
-        }
-
-        (var toDispose, target.Selection.SelectionPath) = (target.Selection.SelectionPath, new VectorPath(originalPath!));
-        toDispose.Dispose();
-        infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
-
-        savedChunks = null;
-        return infos;
-    }
-
-    public override void Dispose()
-    {
-        if (hasEnqueudImages)
-            throw new InvalidOperationException("Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
-
-        if (images is not null)
-        {
-            foreach (var (_, (image, _)) in images)
-            {
-                image.Dispose();
-            }
-        }
-
-        if (savedChunks is not null)
-        {
-            foreach (var (_, chunks) in savedChunks)
-            {
-                chunks.Dispose();
-            }
-        }
-    }
-}

+ 450 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -0,0 +1,450 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Objects;
+using PixiEditor.ChangeableDocument.Changes.Selection;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
+internal class TransformSelected_UpdateableChange : UpdateableChange
+{
+    private readonly bool drawOnMask;
+    private bool keepOriginal;
+    private ShapeCorners masterCorners;
+
+    private List<MemberTransformationData> memberData;
+
+    private VectorPath? originalPath;
+    private RectD originalSelectionBounds;
+    private VecD selectionAwareSize;
+    private VecD tightBoundsSize;
+    private RectD cornersToSelectionOffset;
+    private VecD originalCornersSize;
+
+    private bool isTransformingSelection;
+    private bool hasEnqueudImages = false;
+    private int frame;
+    private bool appliedOnce;
+    private AffectedArea lastAffectedArea;
+
+    private static Paint RegularPaint { get; } = new() { BlendMode = BlendMode.SrcOver };
+
+    [GenerateUpdateableChangeActions]
+    public TransformSelected_UpdateableChange(
+        ShapeCorners masterCorners,
+        bool keepOriginal,
+        Dictionary<Guid, ShapeCorners> memberCorners,
+        bool transformMask,
+        int frame)
+    {
+        memberData = new();
+        foreach (var corners in memberCorners)
+        {
+            memberData.Add(new MemberTransformationData(corners.Key) { MemberCorners = corners.Value });
+        }
+
+        this.masterCorners = masterCorners;
+        this.keepOriginal = keepOriginal;
+        this.drawOnMask = transformMask;
+        this.frame = frame;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (memberData.Count == 0)
+            return false;
+        
+        originalCornersSize = masterCorners.RectSize;
+        RectD tightBoundsWithSelection = default;
+        bool hasSelection = target.Selection.SelectionPath is { IsEmpty: false };
+
+        if (hasSelection)
+        {
+            originalPath = new VectorPath(target.Selection.SelectionPath) { FillType = PathFillType.EvenOdd };
+            tightBoundsWithSelection = originalPath.TightBounds;
+            originalSelectionBounds = tightBoundsWithSelection;
+            selectionAwareSize = tightBoundsWithSelection.Size;
+            isTransformingSelection = true;
+            
+            tightBoundsSize = tightBoundsWithSelection.Size;
+            cornersToSelectionOffset = new RectD(masterCorners.TopLeft - tightBoundsWithSelection.TopLeft, 
+                tightBoundsSize - masterCorners.RectSize);
+        }
+
+        StructureNode firstLayer = target.FindMemberOrThrow(memberData[0].MemberId);
+        RectD tightBounds = firstLayer.GetTightBounds(frame) ?? default;
+
+        if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
+        {
+            tightBounds = vectorLayer.ShapeData?.GeometryAABB ?? default;
+        }
+
+        for (var i = 1; i < memberData.Count; i++)
+        {
+            StructureNode layer = target.FindMemberOrThrow(memberData[i].MemberId);
+            
+            var layerTightBounds = layer.GetTightBounds(frame);
+            
+            if (tightBounds == default)
+            {
+                tightBounds = layerTightBounds.GetValueOrDefault();
+            }
+
+            if (layerTightBounds is not null)
+            {
+                tightBounds = tightBounds.Union(layerTightBounds.Value);
+            }
+        }
+        
+        if (tightBounds == default)
+            return false;
+
+        tightBoundsSize = tightBounds.Size;
+
+        foreach (var member in memberData)
+        {
+            StructureNode layer = target.FindMemberOrThrow(member.MemberId);
+
+            if (layer is IReadOnlyImageNode)
+            {
+                var targetBounds = tightBoundsWithSelection != default ? tightBoundsWithSelection : tightBounds;
+                SetImageMember(target, member, targetBounds, layer);
+            }
+            else if (layer is ITransformableObject transformable)
+            {
+                SetTransformableMember(layer, member, transformable, tightBounds);
+            }
+        }
+
+        return true;
+    }
+
+    private void SetTransformableMember(StructureNode layer, MemberTransformationData member,
+        ITransformableObject transformable, RectD tightBounds)
+    {
+        member.OriginalBounds = tightBounds; 
+        VecD posRelativeToMaster = member.OriginalBounds.Value.TopLeft - masterCorners.TopLeft;
+
+        member.OriginalPos = (VecI)posRelativeToMaster;
+        member.AddTransformableObject(transformable, transformable.TransformationMatrix);
+    }
+
+    private void SetImageMember(Document target, MemberTransformationData member, RectD originalTightBounds,
+        StructureNode layer)
+    {
+        ChunkyImage image =
+            DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+        VectorPath pathToExtract = originalPath;
+        RectD targetBounds = originalTightBounds;
+
+        if (pathToExtract == null)
+        {
+            RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
+            pathToExtract = new VectorPath();
+            pathToExtract.AddRect((RectI)tightBounds);
+        }
+
+        member.OriginalPath = pathToExtract;
+        member.OriginalBounds = targetBounds;
+        var extracted = ExtractArea(image, pathToExtract, member.RoundedOriginalBounds.Value);
+        if (extracted.IsT0)
+            return;
+
+        member.AddImage(extracted.AsT1.image, extracted.AsT1.extractedRect.Pos);
+    }
+
+    [UpdateChangeMethod]
+    public void Update(ShapeCorners masterCorners, bool keepOriginal)
+    {
+        this.keepOriginal = keepOriginal;
+        this.masterCorners = masterCorners;
+
+        var globalMatrixWithSelection = OperationHelper.CreateMatrixFromPoints(masterCorners, originalCornersSize);
+        var tightBoundsGlobalMatrix = OperationHelper.CreateMatrixFromPoints(masterCorners, tightBoundsSize);
+
+        foreach (var member in memberData)
+        {
+            Matrix3X3 localMatrix = tightBoundsGlobalMatrix;
+
+            if (member.IsImage)
+            {
+                localMatrix = 
+                    Matrix3X3.CreateTranslation(
+                        (float)-cornersToSelectionOffset.TopLeft.X, (float)-cornersToSelectionOffset.TopLeft.Y)
+                        .PostConcat(
+                    Matrix3X3.CreateTranslation(
+                    (float)member.OriginalPos.Value.X - (float)member.OriginalBounds.Value.Left,
+                    (float)member.OriginalPos.Value.Y - (float)member.OriginalBounds.Value.Top));
+                
+                localMatrix = localMatrix.PostConcat(selectionAwareSize.Length > 0 ? globalMatrixWithSelection : tightBoundsGlobalMatrix);
+            }
+            else if (member.OriginalMatrix is not null)
+            {
+                if (memberData.Count > 1)
+                {
+                    localMatrix = member.OriginalMatrix.Value;
+                    localMatrix = localMatrix.PostConcat(Matrix3X3.CreateTranslation(
+                            (float)member.OriginalPos.Value.X - (float)member.OriginalBounds.Value.Left,
+                            (float)member.OriginalPos.Value.Y - (float)member.OriginalBounds.Value.Top))
+                        .PostConcat(tightBoundsGlobalMatrix);
+                }
+                else
+                {
+                    localMatrix = Matrix3X3.CreateTranslation(
+                        (float)-member.OriginalBounds.Value.X,
+                        (float)-member.OriginalBounds.Value.Y);
+                    localMatrix = localMatrix.PostConcat(tightBoundsGlobalMatrix);
+                }
+            }
+
+            member.LocalMatrix = localMatrix;
+        }
+    }
+
+    public OneOf<None, (Surface image, RectI extractedRect)> ExtractArea(ChunkyImage image, VectorPath path,
+        RectI pathBounds)
+    {
+        // get rid of transparent areas on edges
+        var memberImageBounds = image.FindChunkAlignedMostUpToDateBounds();
+        if (memberImageBounds is null)
+            return new None();
+        pathBounds = pathBounds.Intersect(memberImageBounds.Value);
+        pathBounds = pathBounds.Intersect(new RectI(VecI.Zero, image.LatestSize));
+        if (pathBounds.IsZeroOrNegativeArea)
+            return new None();
+
+        // shift the clip to account for the image being smaller than the selection
+        VectorPath clipPath = new VectorPath(path) { FillType = PathFillType.EvenOdd };
+        clipPath.Transform(Matrix3X3.CreateTranslation(-pathBounds.X, -pathBounds.Y));
+
+        // draw
+        Surface output = new(pathBounds.Size);
+        output.DrawingSurface.Canvas.Save();
+        output.DrawingSurface.Canvas.ClipPath(clipPath);
+        image.DrawMostUpToDateRegionOn(pathBounds, ChunkResolution.Full, output.DrawingSurface, VecI.Zero);
+        output.DrawingSurface.Canvas.Restore();
+
+        return (output, pathBounds);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        if (appliedOnce)
+            throw new InvalidOperationException("Apply called twice");
+        appliedOnce = true;
+
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            if (member.IsImage)
+            {
+                ChunkyImage memberImage =
+                    DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+                var area = DrawImage(member, memberImage);
+                member.SavedChunks = new(memberImage, memberImage.FindAffectedArea().Chunks);
+                memberImage.CommitChanges();
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId, area, drawOnMask).AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+
+                AffectedArea area = GetTranslationAffectedArea();
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
+            }
+        }
+
+        if (isTransformingSelection)
+        {
+            infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalSelectionBounds,
+                masterCorners, cornersToSelectionOffset, originalCornersSize));
+        }
+        
+        hasEnqueudImages = false;
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            if (member.IsImage)
+            {
+                ChunkyImage targetImage =
+                    DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId,
+                        DrawImage(member, targetImage), drawOnMask)
+                    .AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+
+                AffectedArea translationAffectedArea = GetTranslationAffectedArea();
+                var tmp = new AffectedArea(translationAffectedArea);
+                if (lastAffectedArea.Chunks != null)
+                {
+                    translationAffectedArea.UnionWith(lastAffectedArea);
+                }
+
+                lastAffectedArea = tmp;
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, translationAffectedArea));
+            }
+        }
+
+        if (isTransformingSelection)
+        {
+            infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalSelectionBounds,
+                masterCorners, cornersToSelectionOffset, originalCornersSize));
+        }
+
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            if (member.SavedChunks is not null)
+            {
+                var storageCopy = member.SavedChunks;
+                var chunks =
+                    DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, member.MemberId, drawOnMask, frame,
+                        ref storageCopy);
+
+                member.SavedChunks = null;
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId, chunks, drawOnMask).AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                member.TransformableObject.TransformationMatrix = member.OriginalMatrix!.Value;
+
+                //TODO this is probably wrong
+                AffectedArea area = GetTranslationAffectedArea();
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
+            }
+        }
+
+        if (originalPath != null)
+        {
+            (var toDispose, target.Selection.SelectionPath) =
+                (target.Selection.SelectionPath, new VectorPath(originalPath!));
+            toDispose.Dispose();
+        }
+
+        infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
+        
+        appliedOnce = false;
+
+        return infos;
+    }
+
+    public override void Dispose()
+    {
+        if (hasEnqueudImages)
+            throw new InvalidOperationException(
+                "Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
+
+        foreach (var member in memberData)
+        {
+            member.Dispose();
+        }
+    }
+
+    private AffectedArea GetTranslationAffectedArea()
+    {
+        RectI oldBounds = (RectI)masterCorners.AABBBounds.RoundOutwards();
+
+        HashSet<VecI> chunks = new();
+        VecI topLeftChunk = new VecI((int)oldBounds.Left / ChunkyImage.FullChunkSize,
+            (int)oldBounds.Top / ChunkyImage.FullChunkSize);
+        VecI bottomRightChunk = new VecI((int)oldBounds.Right / ChunkyImage.FullChunkSize,
+            (int)oldBounds.Bottom / ChunkyImage.FullChunkSize);
+
+        for (int x = topLeftChunk.X; x <= bottomRightChunk.X; x++)
+        {
+            for (int y = topLeftChunk.Y; y <= bottomRightChunk.Y; y++)
+            {
+                chunks.Add(new VecI(x, y));
+            }
+        }
+
+        var final = new AffectedArea(chunks);
+        return final;
+    }
+
+    private AffectedArea DrawImage(MemberTransformationData data, ChunkyImage memberImage)
+    {
+        var prevAffArea = memberImage.FindAffectedArea();
+
+        memberImage.CancelChanges();
+
+        if (!keepOriginal)
+            memberImage.EnqueueClearPath(data.OriginalPath!, data.RoundedOriginalBounds);
+        memberImage.EnqueueDrawImage(data.LocalMatrix, data.Image, RegularPaint, false);
+        hasEnqueudImages = true;
+
+        var affectedArea = memberImage.FindAffectedArea();
+        affectedArea.UnionWith(prevAffArea);
+        return affectedArea;
+    }
+}
+
+class MemberTransformationData : IDisposable
+{
+    public Guid MemberId { get; }
+    public ShapeCorners MemberCorners { get; init; }
+
+    public ITransformableObject? TransformableObject { get; private set; }
+    public Matrix3X3? OriginalMatrix { get; private set; }
+
+    public CommittedChunkStorage? SavedChunks { get; set; }
+    public VectorPath? OriginalPath { get; set; }
+    public Surface? Image { get; set; }
+    public RectD? OriginalBounds { get; set; }
+    public VecD? OriginalPos { get; set; }
+    public bool IsImage => Image != null;
+    public bool IsTransformable => TransformableObject != null;
+    public RectI? RoundedOriginalBounds => (RectI)OriginalBounds?.RoundOutwards();
+    public Matrix3X3 LocalMatrix { get; set; }
+
+    public MemberTransformationData(Guid memberId)
+    {
+        MemberId = memberId;
+    }
+
+    public void AddTransformableObject(ITransformableObject transformableObject, Matrix3X3 originalMatrix)
+    {
+        TransformableObject = transformableObject;
+        OriginalMatrix = new Matrix3X3?(originalMatrix);
+    }
+
+    public void AddImage(Surface img, VecI extractedRectPos)
+    {
+        Image = img;
+        OriginalPos = extractedRectPos;
+    }
+
+    public void Dispose()
+    {
+        Image?.Dispose();
+        Image = null;
+        OriginalPath?.Dispose();
+        OriginalPath = null;
+        SavedChunks?.Dispose();
+    }
+}

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs

@@ -23,9 +23,9 @@ internal class DeserializeNodeAdditionalData_Change : Change
     {
         Node node = target.FindNode<Node>(nodeId);
         
-        node.DeserializeData(target, data);
+        var changeInfos = node.DeserializeAdditionalData(target, data);
         ignoreInUndo = false;
-        return new None();
+        return changeInfos;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs

@@ -45,7 +45,7 @@ internal class CenterContent_Change : Change
         foreach (var layerGuid in affectedLayers)
         {
             LayerNode layer = document.FindMemberOrThrow<LayerNode>(layerGuid);
-            RectI? tightBounds = layer.GetTightBounds(frame);
+            RectI? tightBounds = (RectI)layer.GetTightBounds(frame);
             if (tightBounds.HasValue)
             {
                 currentBounds = currentBounds.HasValue ? currentBounds.Value.Union(tightBounds.Value) : tightBounds;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -24,8 +24,8 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
                 var layerBounds = layer.GetTightBounds(frameToClip);
                 if (layerBounds.HasValue)
                 {
-                    bounds ??= layerBounds.Value;
-                    bounds = bounds.Value.Union(layerBounds.Value);
+                    bounds ??= (RectI)layerBounds.Value;
+                    bounds = bounds.Value.Union((RectI)layerBounds.Value);
                 }
             }
         });

+ 26 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs

@@ -4,6 +4,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
+
 internal class SelectionChangeHelper
 {
     public static Selection_ChangeInfo DoSelectionTransform(
@@ -11,8 +12,31 @@ internal class SelectionChangeHelper
     {
         VectorPath newPath = new(originalPath);
 
-        var matrix = Matrix3X3.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y).PostConcat(
-            OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
+        var matrix = Matrix3X3.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y)
+            .PostConcat(
+                OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
+        newPath.Transform(matrix);
+        var toDispose = target.Selection.SelectionPath;
+        target.Selection.SelectionPath = newPath;
+        toDispose.Dispose();
+
+        return new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath));
+    }
+
+    public static IChangeInfo DoSelectionTransform(Document target, VectorPath originalPath,
+        RectD originalPathTightBounds, ShapeCorners to, RectD cornersToSelectionOffset, VecD originalCornersSize)
+    {
+        VectorPath newPath = new(originalPath);
+
+        var matrix =
+            Matrix3X3.CreateTranslation(-(float)cornersToSelectionOffset.X, -(float)cornersToSelectionOffset.Y);
+
+        matrix = matrix.PostConcat(Matrix3X3.CreateTranslation(
+            (float)(-originalPathTightBounds.X),
+            (float)(-originalPathTightBounds.Y)));
+        
+        matrix = matrix.PostConcat(OperationHelper.CreateMatrixFromPoints(to, originalCornersSize));
+        
         newPath.Transform(matrix);
         var toDispose = target.Selection.SelectionPath;
         target.Selection.SelectionPath = newPath;

+ 11 - 13
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -15,32 +15,30 @@ internal class CreateStructureMember_Change : Change
     private Guid newMemberGuid;
 
     private Guid parentGuid;
-    private StructureMemberType type;
+    private Type structureMemberOfType;
 
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
-        StructureMemberType type)
+        Type ofType)
     {
         this.parentGuid = parent;
-        this.type = type;
+        this.structureMemberOfType = ofType;
         newMemberGuid = newGuid;
     }
 
     public override bool InitializeAndValidate(Document target)
     {
+        if(structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+            return false;
+        
         return target.TryFindNode<Node>(parentGuid, out _);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
         out bool ignoreInUndo)
     {
-        StructureNode member = type switch
-        {
-            // TODO: Add support for other types
-            StructureMemberType.Layer => new ImageLayerNode(document.Size) { Id = newMemberGuid },
-            StructureMemberType.Folder => new FolderNode() { Id = newMemberGuid },
-            _ => throw new NotSupportedException(),
-        };
+        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document); 
+        member.Id = newMemberGuid;
 
         document.TryFindNode<Node>(parentGuid, out var parentNode);
 
@@ -71,10 +69,10 @@ internal class CreateStructureMember_Change : Change
 
     private IChangeInfo CreateChangeInfo(StructureNode member)
     {
-        return type switch
+        return member switch
         {
-            StructureMemberType.Layer => CreateLayer_ChangeInfo.FromLayer((LayerNode)member),
-            StructureMemberType.Folder => CreateFolder_ChangeInfo.FromFolder((FolderNode)member),
+            LayerNode layer => CreateLayer_ChangeInfo.FromLayer(layer),
+            FolderNode folderNode => CreateFolder_ChangeInfo.FromFolder(folderNode),
             _ => throw new NotSupportedException(),
         };
     }

+ 128 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -0,0 +1,128 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class RasterizeMember_Change : Change
+{
+    private Guid memberId;
+    
+    private Node originalNode;
+    private Guid createdNodeId;
+    
+    private ConnectionsData originalConnections;
+    
+    [GenerateMakeChangeAction]
+    public RasterizeMember_Change(Guid memberId)
+    {
+        this.memberId = memberId;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.TryFindMember(memberId, out var member) 
+            && member is not IReadOnlyImageNode && member is IRasterizable)
+        {
+            originalNode = member.Clone();
+            originalConnections = NodeOperations.CreateConnectionsData(member);
+            return true;
+        }
+        
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        Node node = target.FindMember(memberId);
+        
+        IRasterizable rasterizable = (IRasterizable)node;
+        
+        ImageLayerNode imageLayer = new ImageLayerNode(target.Size);
+        imageLayer.MemberName = node.DisplayName;
+
+        target.NodeGraph.AddNode(imageLayer);
+        
+        using Surface surface = new Surface(target.Size);
+        rasterizable.Rasterize(surface.DrawingSurface, ChunkResolution.Full, null);
+        
+        var image = imageLayer.GetLayerImageAtFrame(0);
+        image.EnqueueDrawImage(VecI.Zero, surface);
+        image.CommitChanges();
+
+        OutputProperty<Texture>? outputConnection = node.OutputProperties.FirstOrDefault(x => x is OutputProperty<Texture>) as OutputProperty<Texture>;
+        InputProperty<Texture>? outputConnectedInput =
+            outputConnection?.Connections.FirstOrDefault(x => x is InputProperty<Texture>) as InputProperty<Texture>;
+
+        InputProperty<Texture> backgroundInput = imageLayer.Background;
+        OutputProperty<Texture> toAddOutput = imageLayer.Output;
+
+        List<IChangeInfo> changeInfos = new();
+        changeInfos.Add(CreateNode_ChangeInfo.CreateFromNode(imageLayer));
+        changeInfos.AddRange(NodeOperations.AppendMember(outputConnectedInput, toAddOutput, backgroundInput, imageLayer.Id));
+
+        List<(string inputPropName, IOutputProperty connection)> connections = new();
+        
+        foreach (var inputProp in node.InputProperties)
+        {
+            if(inputProp.Connection == null) continue;
+            
+            connections.Add((inputProp.InternalPropertyName, inputProp.Connection));
+        }
+
+        foreach (var conn in connections)
+        {
+            InputProperty? targetInput = imageLayer.GetInputProperty(conn.inputPropName);
+            if (targetInput == null) continue;
+            
+            conn.connection.ConnectTo(targetInput);
+            changeInfos.Add(new ConnectProperty_ChangeInfo(conn.connection.Node.Id, imageLayer.Id, conn.connection.InternalPropertyName, conn.inputPropName));
+        }
+        
+        changeInfos.AddRange(NodeOperations.DetachNode(target.NodeGraph, node));
+        
+        node.Dispose();
+        target.NodeGraph.RemoveNode(node);
+        
+        changeInfos.Add(new DeleteNode_ChangeInfo(node.Id));
+        
+        createdNodeId = imageLayer.Id;
+        
+        ignoreInUndo = false;
+        return changeInfos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        Node node = target.FindMember(createdNodeId);
+        
+        List<IChangeInfo> changeInfos = new();
+        changeInfos.AddRange(NodeOperations.DetachNode(target.NodeGraph, node));
+        
+        node.Dispose();
+        target.NodeGraph.RemoveNode(node);
+        
+        changeInfos.Add(new DeleteNode_ChangeInfo(node.Id));
+        
+        var restoredNode = originalNode.Clone();
+        restoredNode.Id = memberId;
+        
+        target.NodeGraph.AddNode(restoredNode);
+        
+        changeInfos.Add(CreateNode_ChangeInfo.CreateFromNode(restoredNode));
+        
+        changeInfos.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, restoredNode, target.NodeGraph));
+        
+        return changeInfos;   
+    }
+
+    public override void Dispose()
+    {
+        originalNode.Dispose();
+    }
+}

+ 99 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs

@@ -0,0 +1,99 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.Vectors;
+
+internal class SetShapeGeometry_UpdateableChange : UpdateableChange
+{
+    public Guid TargetId { get; set; }
+    public ShapeVectorData Data { get; set; }
+
+    private ShapeVectorData? originalData;
+    
+    private AffectedArea lastAffectedArea;
+
+    [GenerateUpdateableChangeActions]
+    public SetShapeGeometry_UpdateableChange(Guid targetId, ShapeVectorData data)
+    {
+        TargetId = targetId;
+        Data = data;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.TryFindNode<VectorLayerNode>(TargetId, out var node))
+        {
+            originalData = (ShapeVectorData?)node.ShapeData?.Clone();
+            return true;
+        }
+
+        return false;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(ShapeVectorData data)
+    {
+        Data = data;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        var node = target.FindNode<VectorLayerNode>(TargetId);
+        node.ShapeData = Data;
+
+        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        var tmp = new AffectedArea(affected);
+        
+        if (lastAffectedArea.Chunks != null)
+        {
+            affected.UnionWith(lastAffectedArea);
+        }
+        
+        lastAffectedArea = tmp;
+        
+        return new VectorShape_ChangeInfo(node.Id, affected);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        var node = target.FindNode<VectorLayerNode>(TargetId);
+        node.ShapeData = Data;
+        
+        RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+        aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(node.Id, affected);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode<VectorLayerNode>(TargetId);
+        node.ShapeData = originalData;
+
+        AffectedArea affected = new AffectedArea();
+        
+        if (node.ShapeData != null)
+        { 
+            RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
+            aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
+         
+            affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+                (RectI)aabb, ChunkyImage.FullChunkSize));
+        }
+
+        return new VectorShape_ChangeInfo(node.Id, affected);
+    }
+}

+ 36 - 29
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -20,18 +20,18 @@ public class DocumentChangeTracker : IDisposable
         {
             if (!undoStack.Any())
                 return null;
-            List<Change> list = undoStack.Peek();
-            if (list.Count == 0)
+            var list = undoStack.Peek();
+            if (list.changes.Count == 0)
                 return null;
-            return list[^1].ChangeGuid;
+            return list.changes[^1].ChangeGuid;
         }
     }
 
     private UpdateableChange? activeUpdateableChange = null;
     private List<Change>? activePacket = null;
 
-    private Stack<List<Change>> undoStack = new();
-    private Stack<List<Change>> redoStack = new();
+    private Stack<(ActionSource source, List<Change> changes)> undoStack = new();
+    private Stack<(ActionSource source, List<Change> changes)> redoStack = new();
 
     public void Dispose()
     {
@@ -53,13 +53,13 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var list in undoStack)
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
         }
 
         foreach (var list in redoStack)
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
         }
     }
@@ -77,14 +77,14 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var changesToDispose in redoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
         redoStack.Clear();
     }
 
-    private void CompletePacket()
+    private void CompletePacket(ActionSource source)
     {
         if (activePacket is null)
             return;
@@ -92,24 +92,25 @@ public class DocumentChangeTracker : IDisposable
         // maybe merge with previous
         if (activePacket.Count == 1 &&
             undoStack.Count > 0 &&
-            IsHomologous(undoStack.Peek()) &&
-            undoStack.Peek()[^1].IsMergeableWith(activePacket[0]))
+            (undoStack.Peek().source == ActionSource.Automated ||
+            (IsHomologous(undoStack.Peek()) &&
+            undoStack.Peek().changes[^1].IsMergeableWith(activePacket[0]))))
         {
-            undoStack.Peek().Add(activePacket[0]);
+            undoStack.Peek().changes.Add(activePacket[0]);
         }
         else
         {
-            undoStack.Push(activePacket);
+            undoStack.Push((source, activePacket));
         }
 
         activePacket = null;
     }
 
-    private bool IsHomologous(List<Change> changes)
+    private bool IsHomologous((ActionSource source, List<Change> changes) changes)
     {
-        for (int i = 1; i < changes.Count; i++)
+        for (int i = 1; i < changes.changes.Count; i++)
         {
-            if (!changes[i].IsMergeableWith(changes[i - 1]))
+            if (!changes.changes[i].IsMergeableWith(changes.changes[i - 1]))
                 return false;
         }
         return true;
@@ -125,11 +126,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
         }
         List<IChangeInfo> changeInfos = new();
-        List<Change> changePacket = undoStack.Pop();
+        var changePacket = undoStack.Pop();
 
-        for (int i = changePacket.Count - 1; i >= 0; i--)
+        for (int i = changePacket.changes.Count - 1; i >= 0; i--)
         {
-            changePacket[i].Revert(document).Switch(
+            changePacket.changes[i].Revert(document).Switch(
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -149,11 +150,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
         }
         List<IChangeInfo> changeInfos = new();
-        List<Change> changePacket = redoStack.Pop();
+        var changePacket = redoStack.Pop();
 
-        for (int i = 0; i < changePacket.Count; i++)
+        for (int i = 0; i < changePacket.changes.Count; i++)
         {
-            changePacket[i].Apply(document, false, out _).Switch(
+            changePacket.changes[i].Apply(document, false, out _).Switch(
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -172,13 +173,13 @@ public class DocumentChangeTracker : IDisposable
         }
         foreach (var changesToDispose in redoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
         foreach (var changesToDispose in undoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
@@ -255,7 +256,7 @@ public class DocumentChangeTracker : IDisposable
         return info;
     }
 
-    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<IAction> actions)
+    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<(ActionSource, IAction)> actions)
     {
         List<IChangeInfo?> changeInfos = new();
         void AddInfo(OneOf<None, IChangeInfo, List<IChangeInfo>> info) =>
@@ -266,7 +267,7 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var action in actions)
         {
-            switch (action)
+            switch (action.Item2)
             {
                 case IMakeChangeAction act:
                     AddInfo(ProcessMakeChangeAction(act));
@@ -284,7 +285,7 @@ public class DocumentChangeTracker : IDisposable
                     AddInfo(Redo());
                     break;
                 case ChangeBoundary_Action:
-                    CompletePacket();
+                    CompletePacket(action.Item1);
                     break;
                 case DeleteRecordedChanges_Action:
                     DeleteAllChanges();
@@ -298,7 +299,7 @@ public class DocumentChangeTracker : IDisposable
         return changeInfos;
     }
 
-    public async Task<List<IChangeInfo?>> ProcessActions(IReadOnlyList<IAction> actions)
+    public async Task<List<IChangeInfo?>> ProcessActions(List<(ActionSource, IAction)> actions)
     {
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -310,7 +311,7 @@ public class DocumentChangeTracker : IDisposable
         return result;
     }
 
-    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<IAction> actions)
+    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<(ActionSource, IAction)> actions)
     {
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -322,3 +323,9 @@ public class DocumentChangeTracker : IDisposable
         return result;
     }
 }
+
+public enum ActionSource
+{
+    User,
+    Automated
+}

+ 1 - 0
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -42,6 +42,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <Folder Include="Models\" />
     <Folder Include="Utils\" />
   </ItemGroup>
 

+ 51 - 4
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -66,7 +66,7 @@ public class DocumentRenderer
 
         return toDrawOn;
     }
-
+    
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
         RectI? globalClippingRect = null)
     {
@@ -125,7 +125,7 @@ public class DocumentRenderer
         HashSet<Guid> layersToCombine, RectI? globalClippingRect)
     {
         using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
-        NodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
+        IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         try
         {
             return RenderChunkOnGraph(chunkPos, resolution, globalClippingRect, membersOnlyGraph, context);
@@ -135,6 +135,47 @@ public class DocumentRenderer
             return new EmptyChunk();
         }
     }
+    
+    public Texture? RenderLayer(Guid nodeId, ChunkResolution resolution, KeyFrameTime frameTime)
+    {
+        var node = Document.FindNode(nodeId);
+        
+        if (node is null)
+        {
+            return null;
+        }
+        
+        VecI sizeInChunks = Document.Size / resolution.PixelSize();
+        
+        sizeInChunks = new VecI(
+            Math.Max(1, sizeInChunks.X),
+            Math.Max(1, sizeInChunks.Y));
+        
+        VecI size = new VecI(
+            Math.Min(Document.Size.X, resolution.PixelSize() * sizeInChunks.X),
+            Math.Min(Document.Size.Y, resolution.PixelSize() * sizeInChunks.Y));
+        
+        Texture texture = new(size);
+        
+        for (int x = 0; x < sizeInChunks.X; x++)
+        {
+            for (int y = 0; y < sizeInChunks.Y; y++)
+            {
+                VecI chunkPos = new(x, y);
+                RectI globalClippingRect = new(0, 0, Document.Size.X, Document.Size.Y);
+                OneOf<Chunk, EmptyChunk> chunk = RenderChunk(chunkPos, resolution, node, frameTime, globalClippingRect);
+                if (chunk.IsT0)
+                {
+                    VecI pos = chunkPos * resolution.PixelSize(); 
+                    texture.DrawingSurface.Canvas.DrawSurface(
+                        chunk.AsT0.Surface.DrawingSurface,
+                        pos.X, pos.Y, null);
+                }
+            }
+        }
+        
+        return texture;
+    }
 
     private static OneOf<Chunk, EmptyChunk> RenderChunkOnGraph(VecI chunkPos, ChunkResolution resolution,
         RectI? globalClippingRect,
@@ -187,7 +228,13 @@ public class DocumentRenderer
         return chunk;
     }
 
-    private NodeGraph ConstructMembersOnlyGraph(HashSet<Guid> layersToCombine, IReadOnlyNodeGraph fullGraph)
+    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(IReadOnlyNodeGraph fullGraph)
+    {
+        return ConstructMembersOnlyGraph(null, fullGraph); 
+    }
+
+    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(HashSet<Guid>? layersToCombine,
+        IReadOnlyNodeGraph fullGraph)
     {
         NodeGraph membersOnlyGraph = new();
 
@@ -199,7 +246,7 @@ public class DocumentRenderer
 
         fullGraph.TryTraverse(node =>
         {
-            if (node is LayerNode layer && layersToCombine.Contains(layer.Id))
+            if (node is LayerNode layer && (layersToCombine == null || layersToCombine.Contains(layer.Id)))
             {
                 layersInOrder.Insert(0, layer);
             }

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Bridge/DrawingBackendApi.cs

@@ -28,7 +28,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge
             }
             
             _current = backend;
-            _current.RenderingServer = server;
+            _current.RenderingDispatcher = server;
             backend.Setup();
         }
     }

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Bridge/IDrawingBackend.cs

@@ -24,6 +24,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge
         public IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
         public bool IsHardwareAccelerated { get; }
-        public IRenderingServer RenderingServer { get; set; }
+        public IRenderingServer RenderingDispatcher { get; set; }
     }
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IMatrix3x3Implementation.cs

@@ -9,4 +9,5 @@ public interface IMatrix3X3Implementation
     public Matrix3X3 Concat(in Matrix3X3 first, in Matrix3X3 second);
     public Matrix3X3 PostConcat(in Matrix3X3 first, in Matrix3X3 second);
     public VecD MapPoint(Matrix3X3 matrix, int p0, int p1);
+    public VecD MapPoint(Matrix3X3 matrix, VecD point);
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/Operations/ICanvasImplementation.cs

@@ -42,5 +42,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public object GetNativeCanvas(IntPtr objectPointer);
         public void DrawPaint(IntPtr objectPointer, Paint paint);
         public void DrawImage(IntPtr objectPointer, Image image, int x, int y, Paint paint);
+        public Matrix3X3 GetActiveMatrix(IntPtr objectPointer);
     }
 }

+ 15 - 0
src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs

@@ -54,6 +54,11 @@ public struct Matrix3X3 : IEquatable<Matrix3X3>
     /// <summary>Gets or sets the z-perspective.</summary>
     /// <value />
     public float Persp2 { get; set; }
+    
+    public VecD Position => new VecD(TransX, TransY);
+    public VecD Scale => new VecD(ScaleX, ScaleY);
+    public VecD Skew => new VecD(SkewX, SkewY);
+    public VecD Perspective => new VecD(Persp0, Persp1);
 
     public readonly bool Equals(Matrix3X3 obj)
     {
@@ -351,11 +356,21 @@ public struct Matrix3X3 : IEquatable<Matrix3X3>
     {
         return DrawingBackendApi.Current.MatrixImplementation.PostConcat(in this, in globalMatrix);
     }
+    
+    public Matrix3X3 Concat(Matrix3X3 localMatrix)
+    {
+        return DrawingBackendApi.Current.MatrixImplementation.Concat(in this, in localMatrix);
+    }
 
     public VecD MapPoint(int p0, int p1)
     {
         return DrawingBackendApi.Current.MatrixImplementation.MapPoint(this, p0, p1);
     }
+    
+    public VecD MapPoint(VecD point)
+    {
+        return DrawingBackendApi.Current.MatrixImplementation.MapPoint(this, point);
+    }
 
     private static void SetSinCos(ref Matrix3X3 matrix, float sin, float cos)
     {

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/Canvas.cs

@@ -14,6 +14,8 @@ namespace PixiEditor.DrawingApi.Core.Surfaces
     public class Canvas : NativeObject
     {
         public override object Native => DrawingBackendApi.Current.CanvasImplementation.GetNativeCanvas(ObjectPointer);
+        public Matrix3X3 ActiveMatrix => DrawingBackendApi.Current.CanvasImplementation.GetActiveMatrix(ObjectPointer);
+
         public event SurfaceChangedEventHandler? Changed;
 
         public Canvas(IntPtr objPtr) : base(objPtr)

+ 7 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs

@@ -75,6 +75,11 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             canvas.DrawImage(img, x, y, skPaint);
         }
 
+        public Matrix3X3 GetActiveMatrix(IntPtr objectPointer)
+        {
+            return ManagedInstances[objectPointer].TotalMatrix.ToMatrix3X3();
+        }
+
         public int Save(IntPtr objPtr)
         {
             return ManagedInstances[objPtr].Save();
@@ -112,9 +117,10 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
         public void DrawPoints(IntPtr objPtr, PointMode pointMode, Point[] points, Paint paint)
         {
+            SKPoint[] skPoints = CastUtility.UnsafeArrayCast<Point, SKPoint>(points);
             ManagedInstances[objPtr].DrawPoints(
                 (SKPointMode)pointMode,
-                CastUtility.UnsafeArrayCast<Point, SKPoint>(points),
+                skPoints,
                 _paintImpl[paint.ObjectPointer]);
         }
 

+ 6 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaMatrixImplementation.cs

@@ -29,5 +29,11 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             var mapped = matrix.ToSkMatrix().MapPoint(p0, p1);
             return new VecD(mapped.X, mapped.Y);
         }
+
+        public VecD MapPoint(Matrix3X3 matrix, VecD point)
+        {
+            var mapped = matrix.ToSkMatrix().MapPoint((float)point.X, (float)point.Y);
+            return new VecD(mapped.X, mapped.Y);
+        }
     }
 }

+ 1 - 1
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -29,7 +29,7 @@ namespace PixiEditor.DrawingApi.Skia
         
         public bool IsHardwareAccelerated => GraphicsContext != null;
         
-        public IRenderingServer RenderingServer { get; set; }
+        public IRenderingServer RenderingDispatcher { get; set; }
 
         public IColorImplementation ColorImplementation { get; }
         public IImageImplementation ImageImplementation { get; }

+ 11 - 0
src/PixiEditor.SVG/Elements/SvgCircle.cs

@@ -0,0 +1,11 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgCircle() : SvgPrimitive("circle")
+{
+    public SvgProperty<SvgNumericUnit> Cx { get; } = new("cx");
+    public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy");
+
+    public SvgProperty<SvgNumericUnit> R { get; } = new("r");
+}

+ 13 - 0
src/PixiEditor.SVG/Elements/SvgEllipse.cs

@@ -0,0 +1,13 @@
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgEllipse() : SvgPrimitive("ellipse")
+{
+    public SvgProperty<SvgNumericUnit> Cx { get; } = new("cx");
+    public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy"); 
+    
+    public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx"); 
+    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry"); 
+}

+ 13 - 0
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -0,0 +1,13 @@
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IElementContainer
+{
+    public List<SvgElement> Children { get; } = new();
+    public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
+    public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
+    public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+}

+ 22 - 0
src/PixiEditor.SVG/Elements/SvgImage.cs

@@ -0,0 +1,22 @@
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgImage : SvgElement
+{
+    public SvgProperty<SvgNumericUnit> X { get; } = new("x");
+    public SvgProperty<SvgNumericUnit> Y { get; } = new("y");
+    
+    public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
+    public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
+        
+    public SvgProperty<SvgStringUnit> Href { get; } = new("xlink:href");
+    public SvgProperty<SvgLinkUnit> Mask { get; } = new("mask");
+    public SvgProperty<SvgEnumUnit<SvgImageRenderingType>> ImageRendering { get; } = new("image-rendering");
+
+    public SvgImage() : base("image")
+    {
+        RequiredNamespaces.Add("xlink", "http://www.w3.org/1999/xlink");
+    }
+}

+ 12 - 0
src/PixiEditor.SVG/Elements/SvgLine.cs

@@ -0,0 +1,12 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgLine() : SvgPrimitive("line") 
+{
+    public SvgProperty<SvgNumericUnit> X1 { get; } = new("x1");
+    public SvgProperty<SvgNumericUnit> Y1 { get; } = new("y1");
+    
+    public SvgProperty<SvgNumericUnit> X2 { get; } = new("x2");
+    public SvgProperty<SvgNumericUnit> Y2 { get; } = new("y2");
+}

+ 14 - 0
src/PixiEditor.SVG/Elements/SvgMask.cs

@@ -0,0 +1,14 @@
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgMask() : SvgElement("mask"), IElementContainer
+{
+    public SvgProperty<SvgNumericUnit> X { get; } = new("x");
+    public SvgProperty<SvgNumericUnit> Y { get; } = new("y");
+    
+    public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
+    public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
+    public List<SvgElement> Children { get; } = new();
+}

+ 8 - 0
src/PixiEditor.SVG/Elements/SvgPolyline.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgPolyline() : SvgPrimitive("polyline")
+{
+    public SvgArray<SvgNumericUnit> Points { get; } = new SvgArray<SvgNumericUnit>("points");
+}

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

@@ -0,0 +1,12 @@
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
+{
+    public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
+    public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
+    public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
+}

+ 15 - 0
src/PixiEditor.SVG/Elements/SvgRectangle.cs

@@ -0,0 +1,15 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgRectangle() : SvgPrimitive("rect")
+{
+    public SvgProperty<SvgNumericUnit> X { get; } = new("x");
+    public SvgProperty<SvgNumericUnit> Y { get; } = new("y");
+
+    public SvgProperty<SvgNumericUnit> Width { get; } = new("width"); 
+    public SvgProperty<SvgNumericUnit> Height { get; } = new("height"); 
+    
+    public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx");
+    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry");
+}

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

@@ -0,0 +1,10 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgImageRenderingType
+{
+    Auto = 0,
+    Smooth = 1,
+    HighQuality = 2,
+    CrispEdges = 3,
+    Pixelated = 4
+}

+ 6 - 0
src/PixiEditor.SVG/Features/IElementContainer.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.SVG.Features;
+
+public interface IElementContainer
+{
+    List<SvgElement> Children { get; }
+}

+ 8 - 0
src/PixiEditor.SVG/Features/IFillable.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IFillable
+{
+    public SvgProperty<SvgColorUnit> Fill { get; }
+}

+ 9 - 0
src/PixiEditor.SVG/Features/IStrokable.cs

@@ -0,0 +1,9 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IStrokable
+{
+    public SvgProperty<SvgColorUnit> Stroke { get; }
+    public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
+}

+ 8 - 0
src/PixiEditor.SVG/Features/ITransformable.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface ITransformable
+{
+    public SvgProperty<SvgTransformUnit> Transform { get; }
+}

+ 9 - 0
src/PixiEditor.SVG/Helpers/StringExtensions.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.SVG.Helpers;
+
+public static class StringExtensions
+{
+    public static string ToKebabCase(this string str)
+    {
+        return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLower();
+    }
+}

+ 14 - 0
src/PixiEditor.SVG/PixiEditor.SVG.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
+      <ProjectReference Include="..\PixiEditor.Numerics\PixiEditor.Numerics.csproj" />
+    </ItemGroup>
+
+</Project>

+ 3 - 0
src/PixiEditor.SVG/README.md

@@ -0,0 +1,3 @@
+# Introduction 
+
+PixiEditor SVG is a .NET library that is used to read and write SVG files.

+ 57 - 0
src/PixiEditor.SVG/SvgDocument.cs

@@ -0,0 +1,57 @@
+using System.Text;
+using PixiEditor.Numerics;
+using PixiEditor.SVG.Features;
+
+namespace PixiEditor.SVG;
+
+public class SvgDocument(RectD viewBox) : IElementContainer
+{
+    public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
+    public string Version { get; set; } = "1.1";
+    public RectD ViewBox { get; set; } = viewBox;
+    public List<SvgElement> Children { get; } = new();
+
+    public string ToXml()
+    {
+        StringBuilder builder = new();
+        builder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
+        builder.AppendLine(
+            $"<svg xmlns=\"{RootNamespace}\" version=\"{Version}\" viewBox=\"{ViewBox.X} {ViewBox.Y} {ViewBox.Width} {ViewBox.Height}\"");
+
+        Dictionary<string, string> usedNamespaces = new();
+
+        GatherRequiredNamespaces(usedNamespaces, Children);
+
+        foreach (var usedNamespace in usedNamespaces)
+        {
+            builder.AppendLine(
+                $"xmlns:{usedNamespace.Key}=\"{usedNamespace.Value}\"");
+        }
+        
+        builder.AppendLine(">");
+
+        foreach (SvgElement child in Children)
+        {
+            builder.AppendLine(child.ToXml());
+        }
+
+        builder.AppendLine("</svg>");
+
+        return builder.ToString();
+    }
+
+    private void GatherRequiredNamespaces(Dictionary<string, string> usedNamespaces, List<SvgElement> elements)
+    {
+        foreach (SvgElement child in elements)
+        {
+            if (child is IElementContainer container)
+            {
+                GatherRequiredNamespaces(usedNamespaces, container.Children);
+            }
+            foreach (KeyValuePair<string, string> ns in child.RequiredNamespaces)
+            {
+                usedNamespaces[ns.Key] = ns.Value;
+            }
+        }
+    }
+}

+ 50 - 0
src/PixiEditor.SVG/SvgElement.cs

@@ -0,0 +1,50 @@
+using System.Text;
+using PixiEditor.SVG.Features;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG;
+
+public class SvgElement(string tagName)
+{
+    public SvgProperty<SvgStringUnit> Id { get; } = new("id");
+    public Dictionary<string, string> RequiredNamespaces { get; } = new();
+    public string TagName { get; } = tagName;
+
+    public string ToXml()
+    {
+        StringBuilder builder = new();
+        builder.Append($"<{TagName}");
+
+        foreach (var property in GetType().GetProperties())
+        {
+            if (property.PropertyType.IsAssignableTo(typeof(SvgProperty)))
+            {
+                SvgProperty prop = (SvgProperty)property.GetValue(this);
+                if (prop != null)
+                {
+                    if (prop.Unit != null)
+                    {
+                        builder.Append($" {prop.SvgName}=\"{prop.Unit.ToXml()}\"");
+                    }
+                }
+            }
+        }
+        
+        if (this is not IElementContainer container)
+        {
+            builder.Append(" />");
+        }
+        else
+        {
+            builder.Append(">");
+            foreach (SvgElement child in container.Children)
+            {
+                builder.AppendLine(child.ToXml());
+            }
+            
+            builder.Append($"</{TagName}>");
+        }
+
+        return builder.ToString();
+    }
+}

+ 27 - 0
src/PixiEditor.SVG/SvgProperty.cs

@@ -0,0 +1,27 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG;
+
+public abstract class SvgProperty
+{
+    protected SvgProperty(string svgName)
+    {
+        SvgName = svgName;
+    }
+
+    public string SvgName { get; set; }
+    public ISvgUnit? Unit { get; set; }
+}
+
+public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
+{
+    public new T? Unit
+    {
+        get => (T?)base.Unit;
+        set => base.Unit = value;
+    }
+
+    public SvgProperty(string svgName) : base(svgName)
+    {
+    }
+}

+ 11 - 0
src/PixiEditor.SVG/Units/SvgArray.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.SVG.Units;
+
+public class SvgArray<T> : SvgProperty where T : ISvgUnit
+{
+    public T[] Units { get; set; }
+
+    public SvgArray(string svgName, params T[] units) : base(svgName)
+    {
+        Units = units;
+    }
+}

+ 41 - 0
src/PixiEditor.SVG/Units/SvgColorUnit.cs

@@ -0,0 +1,41 @@
+namespace PixiEditor.SVG.Units;
+
+public struct SvgColorUnit : ISvgUnit
+{
+    public string Value { get; set; }
+
+    public SvgColorUnit(string value)
+    {
+        Value = value;
+    }
+
+    public static SvgColorUnit FromHex(string value)
+    {
+        return new SvgColorUnit(value);
+    }
+
+    public static SvgColorUnit FromRgb(int r, int g, int b)
+    {
+        return new SvgColorUnit($"rgb({r},{g},{b})");
+    }
+
+    public static SvgColorUnit FromRgba(int r, int g, int b, double a)
+    {
+        return new SvgColorUnit($"rgba({r},{g},{b},{a})");
+    }
+
+    public static SvgColorUnit FromHsl(int h, int s, int l)
+    {
+        return new SvgColorUnit($"hsl({h},{s}%,{l}%)");
+    }
+
+    public static SvgColorUnit FromHsla(int h, int s, int l, double a)
+    {
+        return new SvgColorUnit($"hsla({h},{s}%,{l}%,{a})");
+    }
+
+    public string ToXml()
+    {
+        return Value;
+    }
+}

+ 18 - 0
src/PixiEditor.SVG/Units/SvgEnumUnit.cs

@@ -0,0 +1,18 @@
+using PixiEditor.SVG.Helpers;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgEnumUnit<T> : ISvgUnit where T : Enum
+{
+    public T Value { get; set; }
+
+    public SvgEnumUnit(T value)
+    {
+        Value = value;
+    }
+
+    public string ToXml()
+    {
+        return Value.ToString().ToKebabCase();
+    }
+}

+ 20 - 0
src/PixiEditor.SVG/Units/SvgLinkUnit.cs

@@ -0,0 +1,20 @@
+using PixiEditor.SVG.Elements;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgLinkUnit : ISvgUnit
+{
+    public string? ObjectReference { get; set; } 
+    public string ToXml()
+    {
+        return ObjectReference != null ? $"url(#{ObjectReference}" : string.Empty;
+    }
+
+    public static SvgLinkUnit FromElement(SvgElement element)
+    {
+        return new SvgLinkUnit
+        {
+            ObjectReference = element.Id.Unit?.Value
+        };
+    }
+}

+ 45 - 0
src/PixiEditor.SVG/Units/SvgNumericUnit.cs

@@ -0,0 +1,45 @@
+using System.Globalization;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
+{
+    public string PostFix { get; } = postFix;
+    public double Value { get; set; } = value;
+
+    public static SvgNumericUnit FromUserUnits(double value)
+    {
+        return new SvgNumericUnit(value, string.Empty);
+    }
+    
+    public static SvgNumericUnit FromPixels(double value)
+    {
+        return new SvgNumericUnit(value, "px");
+    }
+    
+    public static SvgNumericUnit FromInches(double value)
+    {
+        return new SvgNumericUnit(value, "in");
+    }
+    
+    public static SvgNumericUnit FromCentimeters(double value)
+    {
+        return new SvgNumericUnit(value, "cm");
+    }
+    
+    public static SvgNumericUnit FromMillimeters(double value)
+    {
+        return new SvgNumericUnit(value, "mm");
+    }
+    
+    public static SvgNumericUnit FromPercent(double value)
+    {
+        return new SvgNumericUnit(value, "%");
+    }
+
+    public string ToXml()
+    {
+        string invariantValue = Value.ToString(CultureInfo.InvariantCulture);
+        return $"{invariantValue}{PostFix}";
+    }
+}

+ 15 - 0
src/PixiEditor.SVG/Units/SvgStringUnit.cs

@@ -0,0 +1,15 @@
+namespace PixiEditor.SVG.Units;
+
+public struct SvgStringUnit : ISvgUnit
+{
+    public SvgStringUnit(string value)
+    {
+        Value = value;
+    }
+
+    public string Value { get; set; }
+    public string ToXml()
+    {
+        return Value;
+    }
+}

+ 31 - 0
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -0,0 +1,31 @@
+using System.Globalization;
+using System.Numerics;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.SVG.Units;
+
+public struct SvgTransformUnit : ISvgUnit
+{
+    public SvgTransformUnit()
+    {
+    }
+
+    public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
+    
+    public SvgTransformUnit(Matrix3X3 matrixValue)
+    {
+        MatrixValue = matrixValue;
+    }
+    
+    public string ToXml()
+    {
+        string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
+        string translateY = MatrixValue.TransY.ToString(CultureInfo.InvariantCulture);
+        string scaleX = MatrixValue.ScaleX.ToString(CultureInfo.InvariantCulture);
+        string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
+        string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
+        string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
+        
+        return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
+    }
+}

+ 6 - 0
src/PixiEditor.SVG/Units/SvgUnit.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.SVG.Units;
+
+public interface ISvgUnit
+{
+    public string ToXml();
+}

+ 8 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -80,6 +80,10 @@
             <Color x:Key="NumbersCategoryBackgroundColor">#666666</Color>
             <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
+            
+            <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
+            <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
+            <Color x:Key="SnapPointPreviewColor">#68abdf</Color>
 
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
 
@@ -163,6 +167,10 @@
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
 
+            <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}"/>
+            <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}"/>
+            <SolidColorBrush x:Key="SnapPointPreviewBrush" Color="{StaticResource SnapPointPreviewColor}"/>
+            
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <system:Double x:Key="ControlCornerRadiusValue">5</system:Double>

+ 2 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -131,6 +131,8 @@
             <system:String x:Key="icon-swatches">&#xE982;</system:String>
             <system:String x:Key="icon-nodes">&#xe984;</system:String>
             <system:String x:Key="icon-onion">&#xe985;</system:String>
+            
+            <system:String x:Key="icon-snapping">&#xfffd;</system:String>
         </ResourceDictionary>
     </Styles.Resources>
     

+ 3 - 0
src/PixiEditor.Zoombox/Zoombox.cs

@@ -125,6 +125,8 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         remove => RemoveHandler(ViewportMovedEvent, value);
     }
 
+    public event Action<double> ScaleChanged;
+
     public VecD CanvasPos => ToScreenSpace(VecD.Zero);
     public double CanvasX => ToScreenSpace(VecD.Zero).X;
     public double CanvasY => ToScreenSpace(VecD.Zero).Y;
@@ -293,6 +295,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
     {
         VecD realDim = new VecD(Bounds.Width, Bounds.Height);
         RealDimensions = realDim;
+        ScaleChanged?.Invoke(Scale);
         RaiseEvent(new ViewportRoutedEventArgs(
             ViewportMovedEvent,
             Center,

+ 31 - 0
src/PixiEditor.sln

@@ -114,6 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libs", "Libs", "{E8A74431-F
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser", "PixiParser\src\PixiParser\PixiParser.csproj", "{0D3DE5D1-D984-407D-B2A6-7945F011B636}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.SVG", "PixiEditor.SVG\PixiEditor.SVG.csproj", "{786E1F87-4A10-493E-88BD-3F2461DBFCA0}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -845,6 +847,34 @@ Global
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Release|x64.ActiveCfg = Release|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Release|x64.Build.0 = Release|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Release|ARM64.Build.0 = Release|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Steam|x64.Build.0 = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -896,6 +926,7 @@ Global
 		{1CCCA0DF-C0D4-4482-B15D-BB9702726C04} = {E8A74431-F76F-43B1-BC66-CA05E249E6F4}
 		{47BC7BC5-C070-49F4-8C8C-542DEDFC78B5} = {E8A74431-F76F-43B1-BC66-CA05E249E6F4}
 		{0D3DE5D1-D984-407D-B2A6-7945F011B636} = {E8A74431-F76F-43B1-BC66-CA05E249E6F4}
+		{786E1F87-4A10-493E-88BD-3F2461DBFCA0} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

Some files were not shown because too many files changed in this diff