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>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
     public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
+        double rotationRad = 0,
         Paint? paint = null)
         Paint? paint = null)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
-            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, paint);
+            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, paint);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
     }
     }

+ 2 - 2
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -48,7 +48,7 @@ public static class IReadOnlyChunkyImageEx
         Func<VecI, ChunkResolution, DrawingSurface, VecI, Paint?, bool> drawingFunc,
         Func<VecI, ChunkResolution, DrawingSurface, VecI, Paint?, bool> drawingFunc,
         Paint? paint = null)
         Paint? paint = null)
     {
     {
-        surface.Canvas.Save();
+        int count = surface.Canvas.Save();
         surface.Canvas.ClipRect(RectD.Create(pos, fullResRegion.Size));
         surface.Canvas.ClipRect(RectD.Create(pos, fullResRegion.Size));
 
 
         VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
         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
 public struct ShapeCorners
 {
 {
     private const double epsilon = 0.001;
     private const double epsilon = 0.001;
+
     public ShapeCorners(VecD center, VecD size)
     public ShapeCorners(VecD center, VecD size)
     {
     {
         TopLeft = center - size / 2;
         TopLeft = center - size / 2;
@@ -15,6 +16,7 @@ public struct ShapeCorners
         BottomRight = center + size / 2;
         BottomRight = center + size / 2;
         BottomLeft = center + new VecD(-size.X / 2, size.Y / 2);
         BottomLeft = center + new VecD(-size.X / 2, size.Y / 2);
     }
     }
+
     public ShapeCorners(RectD rect)
     public ShapeCorners(RectD rect)
     {
     {
         TopLeft = rect.TopLeft;
         TopLeft = rect.TopLeft;
@@ -22,10 +24,12 @@ public struct ShapeCorners
         BottomRight = rect.BottomRight;
         BottomRight = rect.BottomRight;
         BottomLeft = rect.BottomLeft;
         BottomLeft = rect.BottomLeft;
     }
     }
+
     public VecD TopLeft { get; set; }
     public VecD TopLeft { get; set; }
     public VecD TopRight { get; set; }
     public VecD TopRight { get; set; }
     public VecD BottomLeft { get; set; }
     public VecD BottomLeft { get; set; }
     public VecD BottomRight { get; set; }
     public VecD BottomRight { get; set; }
+
     public bool IsInverted
     public bool IsInverted
     {
     {
         get
         get
@@ -34,9 +38,11 @@ public struct ShapeCorners
             var right = TopRight - BottomRight;
             var right = TopRight - BottomRight;
             var bottom = BottomRight - BottomLeft;
             var bottom = BottomRight - BottomLeft;
             var left = BottomLeft - TopLeft;
             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
     public bool IsLegal
     {
     {
         get
         get
@@ -48,7 +54,8 @@ public struct ShapeCorners
             var bottom = BottomRight - BottomLeft;
             var bottom = BottomRight - BottomLeft;
             var left = BottomLeft - TopLeft;
             var left = BottomLeft - TopLeft;
             var topRight = Math.Sign(top.Cross(right));
             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
         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)
             foreach (VecD vec in lengths)
             {
             {
                 if (vec.LengthSquared < epsilon * epsilon)
                 if (vec.LengthSquared < epsilon * epsilon)
                     return true;
                     return true;
             }
             }
+
             return false;
             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 bool IsRect => Math.Abs((TopLeft - BottomRight).Length - (TopRight - BottomLeft).Length) < epsilon;
     public VecD RectSize => new((TopLeft - TopRight).Length, (TopLeft - BottomLeft).Length);
     public VecD RectSize => new((TopLeft - TopRight).Length, (TopLeft - BottomLeft).Length);
     public VecD RectCenter => (TopLeft - BottomRight) / 2 + BottomRight;
     public VecD RectCenter => (TopLeft - BottomRight) / 2 + BottomRight;
+
     public double RectRotation =>
     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
         get
         {
         {
@@ -96,6 +105,7 @@ public struct ShapeCorners
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
         }
         }
     }
     }
+
     public RectD AABBBounds
     public RectD AABBBounds
     {
     {
         get
         get
@@ -120,7 +130,8 @@ public struct ShapeCorners
         var deltaBottomRight = point - BottomRight;
         var deltaBottomRight = point - BottomRight;
         var deltaBottomLeft = point - BottomLeft;
         var deltaBottomLeft = point - BottomLeft;
 
 
-        if (deltaTopRight.IsNaNOrInfinity() || deltaTopLeft.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity())
+        if (deltaTopRight.IsNaNOrInfinity() || deltaTopLeft.IsNaNOrInfinity() || deltaBottomRight.IsNaNOrInfinity() ||
+            deltaBottomRight.IsNaNOrInfinity())
             return false;
             return false;
 
 
         var crossTop = Math.Sign(top.Cross(deltaTopLeft));
         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) => !(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)
     public bool AlmostEquals(ShapeCorners other, double epsilon = 0.001)
@@ -203,7 +215,7 @@ public struct ShapeCorners
             BottomLeft.AlmostEquals(other.BottomLeft, epsilon) &&
             BottomLeft.AlmostEquals(other.BottomLeft, epsilon) &&
             BottomRight.AlmostEquals(other.BottomRight, epsilon);
             BottomRight.AlmostEquals(other.BottomRight, epsilon);
     }
     }
-    
+
     public bool Intersects(RectD rect)
     public bool Intersects(RectD rect)
     {
     {
         // Get all corners
         // Get all corners
@@ -245,4 +257,17 @@ public struct ShapeCorners
         // All projections overlap, so the shapes intersect
         // All projections overlap, so the shapes intersect
         return true;
         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 ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;
+
 public class EllipseHelper
 public class EllipseHelper
 {
 {
     /// <summary>
     /// <summary>
     /// Separates the ellipse's inner area into a bunch of horizontal lines and one big rectangle for drawing.
     /// Separates the ellipse's inner area into a bunch of horizontal lines and one big rectangle for drawing.
     /// </summary>
     /// </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)
         if (ellipse.Count == 0)
             return (new(), RectI.Empty);
             return (new(), RectI.Empty);
@@ -51,14 +54,15 @@ public class EllipseHelper
                 }
                 }
             }
             }
         }
         }
+
         return (lines, inscribedRect);
         return (lines, inscribedRect);
     }
     }
-    
+
     /// <summary>
     /// <summary>
     /// Splits the ellipse into a bunch of horizontal lines.
     /// 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.
     /// 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>
     /// </summary>
-    public static List<VecI> SplitEllipseIntoLines(List<VecI> ellipse)
+    public static List<VecI> SplitEllipseIntoLines(HashSet<VecI> ellipse)
     {
     {
         List<VecI> lines = new();
         List<VecI> lines = new();
         var sorted = ellipse.OrderBy(
         var sorted = ellipse.OrderBy(
@@ -79,25 +83,31 @@ public class EllipseHelper
                 minX = int.MaxValue;
                 minX = int.MaxValue;
                 maxX = int.MinValue;
                 maxX = int.MinValue;
             }
             }
+
             minX = Math.Min(point.X, minX);
             minX = Math.Min(point.X, minX);
             maxX = Math.Max(point.X, maxX);
             maxX = Math.Max(point.X, maxX);
             prev = point;
             prev = point;
         }
         }
+
         if (prev != null)
         if (prev != null)
         {
         {
             lines.Add(new(minX, prev.Value.Y));
             lines.Add(new(minX, prev.Value.Y));
             lines.Add(new(maxX, prev.Value.Y));
             lines.Add(new(maxX, prev.Value.Y));
         }
         }
+
         return lines;
         return lines;
     }
     }
-    
-    public static List<VecI> GenerateEllipseFromRect(RectI rect)
+
+    public static HashSet<VecI> GenerateEllipseFromRect(RectI rect, double rotationRad = 0)
     {
     {
         if (rect.IsZeroOrNegativeArea)
         if (rect.IsZeroOrNegativeArea)
             return new();
             return new();
         float radiusX = (rect.Width - 1) / 2.0f;
         float radiusX = (rect.Width - 1) / 2.0f;
         float radiusY = (rect.Height - 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>
     /// <summary>
@@ -114,14 +124,14 @@ public class EllipseHelper
     /// Center is at (2; 2). It's a place where 4 pixels meet
     /// 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
     /// 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>
     /// </summary>
-    public static List<VecI> GenerateMidpointEllipse(
+    public static HashSet<VecI> GenerateMidpointEllipse(
         double halfWidth,
         double halfWidth,
         double halfHeight,
         double halfHeight,
         double centerX,
         double centerX,
         double centerY,
         double centerY,
-        List<VecI>? listToFill = null)
+        HashSet<VecI>? listToFill = null)
     {
     {
-        listToFill ??= new List<VecI>();
+        listToFill ??= new HashSet<VecI>();
         if (halfWidth < 1 || halfHeight < 1)
         if (halfWidth < 1 || halfHeight < 1)
         {
         {
             AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
             AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
@@ -156,8 +166,7 @@ public class EllipseHelper
             double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
             double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
             double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
             double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
             currentSlope = -(derivativeX / derivativeY);
             currentSlope = -(derivativeX / derivativeY);
-        }
-        while (currentSlope > -1 && currentY - centerY > 0.5);
+        } while (currentSlope > -1 && currentY - centerY > 0.5);
 
 
         // from PI/4 to 0
         // from PI/4 to 0
         while (currentY - centerY >= 0)
         while (currentY - centerY >= 0)
@@ -176,7 +185,122 @@ public class EllipseHelper
         return listToFill;
         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 left = (int)Math.Floor(centerX - halfWidth);
         int top = (int)Math.Floor(centerY - halfHeight);
         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 xFloor = (int)Math.Floor(x);
         int yFloor = (int)Math.Floor(y);
         int yFloor = (int)Math.Floor(y);
@@ -220,4 +344,14 @@ public class EllipseHelper
             coordinates.Add(new VecI(xFloor, yFloorInv));
             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 strokeColor;
     private readonly Color fillColor;
     private readonly Color fillColor;
     private readonly int strokeWidth;
     private readonly int strokeWidth;
+    private readonly double rotation;
     private readonly Paint paint;
     private readonly Paint paint;
     private bool init = false;
     private bool init = false;
     private VectorPath? outerPath;
     private VectorPath? outerPath;
     private VectorPath? innerPath;
     private VectorPath? innerPath;
+    
+    private VectorPath ellipseOutline;
     private Point[]? ellipse;
     private Point[]? ellipse;
     private Point[]? ellipseFill;
     private Point[]? ellipseFill;
     private RectI? ellipseFillRect;
     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.location = location;
         this.strokeColor = strokeColor;
         this.strokeColor = strokeColor;
         this.fillColor = fillColor;
         this.fillColor = fillColor;
         this.strokeWidth = strokeWidth;
         this.strokeWidth = strokeWidth;
+        this.rotation = rotationRad;
         this.paint = paint?.Clone() ?? new Paint();
         this.paint = paint?.Clone() ?? new Paint();
     }
     }
 
 
@@ -37,12 +41,21 @@ internal class EllipseOperation : IMirroredDrawOperation
         init = true;
         init = true;
         if (strokeWidth == 1)
         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
         else
@@ -67,25 +80,52 @@ internal class EllipseOperation : IMirroredDrawOperation
 
 
         if (strokeWidth == 1)
         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
         else
         {
         {
             if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
             if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
             {
             {
                 surf.Canvas.Save();
                 surf.Canvas.Save();
+                surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.ClipPath(innerPath!);
                 surf.Canvas.ClipPath(innerPath!);
                 surf.Canvas.DrawColor(fillColor, paint.BlendMode);
                 surf.Canvas.DrawColor(fillColor, paint.BlendMode);
                 surf.Canvas.Restore();
                 surf.Canvas.Restore();
             }
             }
             surf.Canvas.Save();
             surf.Canvas.Save();
+            surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
             surf.Canvas.DrawColor(strokeColor, paint.BlendMode);
             surf.Canvas.DrawColor(strokeColor, paint.BlendMode);
@@ -96,14 +136,18 @@ internal class EllipseOperation : IMirroredDrawOperation
 
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     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)
         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)
     public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
@@ -113,7 +157,7 @@ internal class EllipseOperation : IMirroredDrawOperation
             newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
             newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
             newLocation = (RectI)newLocation.ReflectY((double)horAxisY).Round();
             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()
     public void Dispose()

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

@@ -176,15 +176,19 @@ public static class OperationHelper
         return chunks;
         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;
         double stretchX = radiusX / radiusY;
         var (left, right) = CreateStretchedHexagon(pos, radiusY, stretchX);
         var (left, right) = CreateStretchedHexagon(pos, radiusY, stretchX);
+        left = left.AsRotated(rotation, pos);
+        right = right.AsRotated(rotation, pos);
+        
         var chunks = FindChunksFullyInsideQuadrilateral(left, chunkSize);
         var chunks = FindChunksFullyInsideQuadrilateral(left, chunkSize);
         chunks.UnionWith(FindChunksFullyInsideQuadrilateral(right, chunkSize));
         chunks.UnionWith(FindChunksFullyInsideQuadrilateral(right, chunkSize));
         return chunks;
         return chunks;
     }
     }
-
+    
     public static HashSet<VecI> FindChunksTouchingQuadrilateral(ShapeCorners corners, int chunkSize)
     public static HashSet<VecI> FindChunksTouchingQuadrilateral(ShapeCorners corners, int chunkSize)
     {
     {
         if (corners.IsRect && Math.Abs(corners.RectRotation) < 0.0001)
         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));
             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)
         if (tightBounds is null)
             return 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));
             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) =>
     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>
     /// <returns>True if the node could be found, otherwise false.</returns>
     public bool TryFindNode<T>(Guid id, out T node) where T : Node
     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;
         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;
 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<Texture?> CustomMask { get; }
     public InputProperty<bool> MaskIsVisible { get; }
     public InputProperty<bool> MaskIsVisible { get; }
     public string MemberName { get; set; }
     public string MemberName { get; set; }
-    public RectI? GetTightBounds(KeyFrameTime frameTime);
-    
+    public RectD? GetTightBounds(KeyFrameTime frameTime);
     public ChunkyImage? EmbeddedMask { get; }
     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;
         return Output.Value;
     }
     }
 
 
-    public override RectI? GetTightBounds(KeyFrameTime frameTime)
+    public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
     {
         RectI bounds = new RectI();
         RectI bounds = new RectI();
         if(Content.Connection != null)
         if(Content.Connection != null)
@@ -111,7 +111,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
             {
             {
                 if (n is ImageLayerNode imageLayerNode)
                 if (n is ImageLayerNode imageLayerNode)
                 {
                 {
-                    RectI? imageBounds = imageLayerNode.GetTightBounds(frameTime);
+                    RectI? imageBounds = (RectI?)imageLayerNode.GetTightBounds(frameTime);
                     if (imageBounds != null)
                     if (imageBounds != null)
                     {
                     {
                         bounds = bounds.Union(imageBounds.Value);
                         bounds = bounds.Union(imageBounds.Value);
@@ -121,10 +121,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
                 return true;
                 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()
     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 VecI size;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
-    
+
 
 
     protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces =
     protected Dictionary<(ChunkResolution, int), Texture> workingSurfaces =
         new Dictionary<(ChunkResolution, int), Texture>();
         new Dictionary<(ChunkResolution, int), Texture>();
@@ -51,147 +51,54 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         this.size = size;
         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)
         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;
             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,
     // 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
     // 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();
         VecI imageChunksSize = frameImage.LatestSize / context.ChunkResolution.PixelSize();
         bool requiresTopLeft = context.ChunkToUpdate.X > 0 || context.ChunkToUpdate.Y > 0;
         bool requiresTopLeft = context.ChunkToUpdate.X > 0 || context.ChunkToUpdate.Y > 0;
         bool requiresTop = context.ChunkToUpdate.Y > 0;
         bool requiresTop = context.ChunkToUpdate.Y > 0;
@@ -240,51 +147,71 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
         if (requiresTopLeft)
         if (requiresTopLeft)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(-1, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(-1, -1), paint);
         }
         }
 
 
         if (requiresTop)
         if (requiresTop)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(0, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(0, -1), paint);
         }
         }
 
 
         if (requiresLeft)
         if (requiresLeft)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 0));
+            DrawChunk(frameImage, context, tempSurface, new VecI(-1, 0), paint);
         }
         }
 
 
         if (requiresTopRight)
         if (requiresTopRight)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, -1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, -1), paint);
         }
         }
 
 
         if (requiresRight)
         if (requiresRight)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, 0));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, 0), paint);
         }
         }
 
 
         if (requiresBottomRight)
         if (requiresBottomRight)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(1, 1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(1, 1), paint);
         }
         }
 
 
         if (requiresBottom)
         if (requiresBottom)
         {
         {
-            DrawChunk(frameImage, context, tempSurface, new VecI(0, 1));
+            DrawChunk(frameImage, context, tempSurface, new VecI(0, 1), paint);
         }
         }
 
 
         if (requiresBottomLeft)
         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;
         VecI chunkPos = context.ChunkToUpdate + vecI;
         if (frameImage.LatestOrCommittedChunkExists(chunkPos))
         if (frameImage.LatestOrCommittedChunkExists(chunkPos))
@@ -294,7 +221,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 context.ChunkResolution,
                 context.ChunkResolution,
                 tempSurface.DrawingSurface,
                 tempSurface.DrawingSurface,
                 chunkPos * context.ChunkResolution.PixelSize(),
                 chunkPos * context.ChunkResolution.PixelSize(),
-                blendPaint);
+                paint);
         }
         }
     }
     }
 
 
@@ -313,7 +240,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     protected override bool CacheChanged(RenderingContext context)
     protected override bool CacheChanged(RenderingContext context)
     {
     {
         var frame = GetFrameWithImage(context.FrameTime);
         var frame = GetFrameWithImage(context.FrameTime);
-        
+
         return base.CacheChanged(context) || frame?.RequiresUpdate == true;
         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.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 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;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
 public abstract class LayerNode : StructureNode, IReadOnlyLayerNode
 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()
     public Node Clone()
     {
     {
         var clone = CreateCopy();
         var clone = CreateCopy();
+        clone.DisplayName = DisplayName;
         clone.Id = Guid.NewGuid();
         clone.Id = Guid.NewGuid();
         clone.Position = Position;
         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.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 
 [NodeInfo("DistributePoints")]
 [NodeInfo("DistributePoints")]
-public class DistributePointsNode : ShapeNode<PointsData>
+public class DistributePointsNode : ShapeNode<PointsVectorData>
 {
 {
     public InputProperty<int> MaxPointCount { get; }
     public InputProperty<int> MaxPointCount { get; }
 
 
@@ -19,12 +18,12 @@ public class DistributePointsNode : ShapeNode<PointsData>
         Seed = CreateInput("Seed", "SEED", 0);
         Seed = CreateInput("Seed", "SEED", 0);
     }
     }
 
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
     {
         return GetPointsRandomly(context.DocumentSize);
         return GetPointsRandomly(context.DocumentSize);
     }
     }
 
 
-    private PointsData GetPointsRandomly(VecI size)
+    private PointsVectorData GetPointsRandomly(VecI size)
     {
     {
         var seed = Seed.Value;
         var seed = Seed.Value;
         var random = new Random(seed);
         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));
             finalPoints.Add(new VecD(random.NextDouble() * size.X, random.NextDouble() * size.Y));
         }
         }
         
         
-        var shapeData = new PointsData(finalPoints);
+        var shapeData = new PointsVectorData(finalPoints);
         return shapeData;
         return shapeData;
     }
     }
 
 

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

@@ -2,12 +2,11 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 
 [NodeInfo("Ellipse")]
 [NodeInfo("Ellipse")]
-public class EllipseNode : ShapeNode<EllipseData>
+public class EllipseNode : ShapeNode<EllipseVectorData>
 {
 {
     public InputProperty<VecD> Position { get; }
     public InputProperty<VecD> Position { get; }
     public InputProperty<VecD> Radius { get; }
     public InputProperty<VecD> Radius { get; }
@@ -25,9 +24,9 @@ public class EllipseNode : ShapeNode<EllipseData>
         StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
         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 };
             { 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;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 
@@ -13,13 +13,16 @@ public class RasterizeShapeNode : Node
 {
 {
     public OutputProperty<Texture> Image { get; }
     public OutputProperty<Texture> Image { get; }
 
 
-    public InputProperty<ShapeData> Data { get; }
+    public InputProperty<ShapeVectorData> Data { get; }
+
+
+    protected override bool AffectedByChunkResolution => true;
 
 
 
 
     public RasterizeShapeNode()
     public RasterizeShapeNode()
     {
     {
         Image = CreateOutput<Texture>("Image", "IMAGE", null);
         Image = CreateOutput<Texture>("Image", "IMAGE", null);
-        Data = CreateInput<ShapeData>("Points", "SHAPE", null);
+        Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
     }
     }
 
 
     protected override Texture? OnExecute(RenderingContext context)
     protected override Texture? OnExecute(RenderingContext context)
@@ -32,7 +35,7 @@ public class RasterizeShapeNode : Node
         var size = context.DocumentSize;
         var size = context.DocumentSize;
         var image = RequestTexture(0, size);
         var image = RequestTexture(0, size);
         
         
-        shape.Rasterize(image.DrawingSurface);
+        shape.RasterizeTransformed(image.DrawingSurface, context.ChunkResolution, null);
 
 
         Image.Value = image;
         Image.Value = image;
         
         

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

@@ -2,14 +2,13 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 
 [NodeInfo("RemoveClosePoints")]
 [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; }
     public InputProperty<double> MinDistance { get; }
 
 
@@ -17,12 +16,12 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
 
 
     public RemoveClosePointsNode()
     public RemoveClosePointsNode()
     {
     {
-        Input = CreateInput<PointsData>("Input", "POINTS", null);
+        Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
         MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
         MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
         Seed = CreateInput("Seed", "SEED", 0);
         Seed = CreateInput("Seed", "SEED", 0);
     }
     }
 
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
     {
         var data = Input.Value;
         var data = Input.Value;
 
 
@@ -64,7 +63,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
             newPoints.Add(availablePoints[0]);
             newPoints.Add(availablePoints[0]);
         }
         }
 
 
-        var finalData = new PointsData(newPoints);
+        var finalData = new PointsVectorData(newPoints);
 
 
         return finalData;
         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.DrawingApi.Core;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 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; }
     public OutputProperty<T> Output { get; }
     
     
@@ -28,11 +29,11 @@ public abstract class ShapeNode<T> : Node where T : ShapeData
     
     
     protected abstract T? GetShapeData(RenderingContext context);
     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);
         Texture texture = RequestTexture(0, size);
         
         
-        data.Rasterize(texture.DrawingSurface);
+        vectorData.RasterizeTransformed(texture.DrawingSurface, ChunkResolution.Full, null);
         
         
         return texture;
         return texture;
     }
     }

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

@@ -1,6 +1,8 @@
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
@@ -27,6 +29,10 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     public OutputProperty<Texture?> FilterlessOutput { get; }
     public OutputProperty<Texture?> FilterlessOutput { get; }
 
 
     public ChunkyImage? EmbeddedMask { get; set; }
     public ChunkyImage? EmbeddedMask { get; set; }
+    public virtual ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return new ShapeCorners(GetTightBounds(frameTime).GetValueOrDefault());
+    }
 
 
     public string MemberName
     public string MemberName
     {
     {
@@ -144,6 +150,9 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         int width = (int)(chunkSize);
         int width = (int)(chunkSize);
         int height = (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);
         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);
         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()
     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;
         this.frame = frame;
         // TODO: pos is unused, check if it should be added to positions
         // 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]
     [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.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Changes.Structure;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -15,6 +17,7 @@ internal class CombineStructureMembersOnto_Change : Change
 
 
     private Guid targetLayer;
     private Guid targetLayer;
     private CommittedChunkStorage? originalChunks;
     private CommittedChunkStorage? originalChunks;
+    
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer, int frame)
     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,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
         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>();
         var chunksToCombine = new HashSet<VecI>();
         foreach (var guid in layersToCombine)
         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();
         toDrawOnImage.EnqueueClear();
 
 
         DocumentRenderer renderer = new(target);
         DocumentRenderer renderer = new(target);
 
 
         AffectedArea affArea = new();
         AffectedArea affArea = new();
-        DrawingBackendApi.Current.RenderingServer.Invoke(() =>
+        DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
         {
             RectI? globalClippingRect = new RectI(0, 0, target.Size.X, target.Size.Y);
             RectI? globalClippingRect = new RectI(0, 0, target.Size.X, target.Size.Y);
             foreach (var chunk in chunksToCombine)
             foreach (var chunk in chunksToCombine)
@@ -100,7 +115,27 @@ internal class CombineStructureMembersOnto_Change : Change
 
 
 
 
         ignoreInUndo = false;
         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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
@@ -109,7 +144,11 @@ internal class CombineStructureMembersOnto_Change : Change
         var affectedArea =
         var affectedArea =
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(toDrawOn.GetLayerImageAtFrame(frame),
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(toDrawOn.GetLayerImageAtFrame(frame),
                 ref originalChunks);
                 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()
     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;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-internal class DrawEllipse_UpdateableChange : UpdateableChange
+internal class DrawRasterEllipse_UpdateableChange : UpdateableChange
 {
 {
     private readonly Guid memberGuid;
     private readonly Guid memberGuid;
     private RectI location;
     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 readonly bool drawOnMask;
     private int frame;
     private int frame;
 
 
     private CommittedChunkStorage? storedChunks;
     private CommittedChunkStorage? storedChunks;
 
 
     [GenerateUpdateableChangeActions]
     [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.memberGuid = memberGuid;
         this.location = location;
         this.location = location;
+        this.rotation = rotationRad;
         this.strokeColor = strokeColor;
         this.strokeColor = strokeColor;
         this.fillColor = fillColor;
         this.fillColor = fillColor;
         this.strokeWidth = strokeWidth;
         this.strokeWidth = strokeWidth;
@@ -28,9 +30,13 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
     }
     }
 
 
     [UpdateChangeMethod]
     [UpdateChangeMethod]
-    public void Update(RectI location)
+    public void Update(RectI location, double rotationRad, Color strokeColor, Color fillColor, int strokeWidth)
     {
     {
         this.location = location;
         this.location = location;
+        rotation = rotationRad;
+        this.strokeColor = strokeColor;
+        this.fillColor = fillColor;
+        this.strokeWidth = strokeWidth;
     }
     }
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
@@ -47,7 +53,7 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
         if (!location.IsZeroOrNegativeArea)
         if (!location.IsZeroOrNegativeArea)
         {
         {
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
-            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth);
+            targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth, rotation);
         }
         }
 
 
         var affectedArea = targetImage.FindAffectedArea();
         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;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
-internal class DrawLine_UpdateableChange : UpdateableChange
+internal class DrawRasterLine_UpdateableChange : UpdateableChange
 {
 {
     private readonly Guid memberGuid;
     private readonly Guid memberGuid;
     private VecI from;
     private VecI from;
@@ -18,7 +18,7 @@ internal class DrawLine_UpdateableChange : UpdateableChange
     private int frame;
     private int frame;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
-    public DrawLine_UpdateableChange
+    public DrawRasterLine_UpdateableChange
         (Guid memberGuid, VecI from, VecI to, int strokeWidth, Color color, StrokeCap caps, bool drawOnMask, int frame)
         (Guid memberGuid, VecI from, VecI to, int strokeWidth, Color color, StrokeCap caps, bool drawOnMask, int frame)
     {
     {
         this.memberGuid = memberGuid;
         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;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
-internal class DrawRectangle_UpdateableChange : UpdateableChange
+internal class DrawRasterRectangle_UpdateableChange : UpdateableChange
 {
 {
     private readonly Guid memberGuid;
     private readonly Guid memberGuid;
     private ShapeData rect;
     private ShapeData rect;
@@ -11,7 +11,7 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
     private int frame;
     private int frame;
     
     
     [GenerateUpdateableChangeActions]
     [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.memberGuid = memberGuid;
         this.rect = rectangle;
         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
             // If it should draw on the mask, the mask can't be null
             true when member.EmbeddedMask is null => false,
             true when member.EmbeddedMask is null => false,
             // If it should not draw on the mask, the member can't be a folder
             // 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
             _ => 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.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
@@ -63,10 +64,19 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         {
         {
             image.EnqueueDrawBresenhamLine(from, to, color, BlendMode.Src);
             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
         else
         {
         {
             var rect = new RectI(to - new VecI(strokeWidth / 2), new VecI(strokeWidth));
             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);
             image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
         }
         }
         var affChunks = image.FindAffectedArea(opCount);
         var affChunks = image.FindAffectedArea(opCount);
@@ -85,13 +95,13 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             else
             else
             {
             {
                 var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
                 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;
             return;
         }
         }
 
 
         var firstRect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
         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++)
         for (int i = 1; i < points.Count; i++)
         {
         {
@@ -102,7 +112,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             else
             else
             {
             {
                 var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
                 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);
                 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;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal class ShiftLayer_UpdateableChange : UpdateableChange
 internal class ShiftLayer_UpdateableChange : UpdateableChange
 {
 {
     private List<Guid> layerGuids;
     private List<Guid> layerGuids;
     private bool keepOriginal;
     private bool keepOriginal;
     private VecI delta;
     private VecI delta;
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
-    
+
     private List<IChangeInfo> _tempChanges = new();
     private List<IChangeInfo> _tempChanges = new();
     private int frame;
     private int frame;
 
 
@@ -36,7 +37,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         {
         {
             if (!target.HasMember(layer)) return false;
             if (!target.HasMember(layer)) return false;
         }
         }
-        
+
         return true;
         return true;
     }
     }
 
 
@@ -47,18 +48,26 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         this.keepOriginal = keepOriginal;
         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?>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage?>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         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);
             var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            // TODO: Add support for different Layer types
             var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
             var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-            
+
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
-            
+
             originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
             originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
             image.CommitChanges();
             image.CommitChanges();
         }
         }
@@ -73,10 +82,17 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
 
 
         foreach (var layerGuid in layerGuids)
         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);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
             _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
             _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
         }
-        
+
         return _tempChanges;
         return _tempChanges;
     }
     }
 
 
@@ -85,12 +101,19 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         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);
             var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
             changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         }
-        
+
         return changes;
         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 node = target.FindNode<Node>(nodeId);
         
         
-        node.DeserializeData(target, data);
+        var changeInfos = node.DeserializeAdditionalData(target, data);
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return new None();
+        return changeInfos;
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     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)
         foreach (var layerGuid in affectedLayers)
         {
         {
             LayerNode layer = document.FindMemberOrThrow<LayerNode>(layerGuid);
             LayerNode layer = document.FindMemberOrThrow<LayerNode>(layerGuid);
-            RectI? tightBounds = layer.GetTightBounds(frame);
+            RectI? tightBounds = (RectI)layer.GetTightBounds(frame);
             if (tightBounds.HasValue)
             if (tightBounds.HasValue)
             {
             {
                 currentBounds = currentBounds.HasValue ? currentBounds.Value.Union(tightBounds.Value) : tightBounds;
                 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);
                 var layerBounds = layer.GetTightBounds(frameToClip);
                 if (layerBounds.HasValue)
                 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;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
 namespace PixiEditor.ChangeableDocument.Changes.Selection;
+
 internal class SelectionChangeHelper
 internal class SelectionChangeHelper
 {
 {
     public static Selection_ChangeInfo DoSelectionTransform(
     public static Selection_ChangeInfo DoSelectionTransform(
@@ -11,8 +12,31 @@ internal class SelectionChangeHelper
     {
     {
         VectorPath newPath = new(originalPath);
         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);
         newPath.Transform(matrix);
         var toDispose = target.Selection.SelectionPath;
         var toDispose = target.Selection.SelectionPath;
         target.Selection.SelectionPath = newPath;
         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 newMemberGuid;
 
 
     private Guid parentGuid;
     private Guid parentGuid;
-    private StructureMemberType type;
+    private Type structureMemberOfType;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
-        StructureMemberType type)
+        Type ofType)
     {
     {
         this.parentGuid = parent;
         this.parentGuid = parent;
-        this.type = type;
+        this.structureMemberOfType = ofType;
         newMemberGuid = newGuid;
         newMemberGuid = newGuid;
     }
     }
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
+        if(structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+            return false;
+        
         return target.TryFindNode<Node>(parentGuid, out _);
         return target.TryFindNode<Node>(parentGuid, out _);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
         out bool ignoreInUndo)
         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);
         document.TryFindNode<Node>(parentGuid, out var parentNode);
 
 
@@ -71,10 +69,10 @@ internal class CreateStructureMember_Change : Change
 
 
     private IChangeInfo CreateChangeInfo(StructureNode member)
     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(),
             _ => 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())
             if (!undoStack.Any())
                 return null;
                 return null;
-            List<Change> list = undoStack.Peek();
-            if (list.Count == 0)
+            var list = undoStack.Peek();
+            if (list.changes.Count == 0)
                 return null;
                 return null;
-            return list[^1].ChangeGuid;
+            return list.changes[^1].ChangeGuid;
         }
         }
     }
     }
 
 
     private UpdateableChange? activeUpdateableChange = null;
     private UpdateableChange? activeUpdateableChange = null;
     private List<Change>? activePacket = 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()
     public void Dispose()
     {
     {
@@ -53,13 +53,13 @@ public class DocumentChangeTracker : IDisposable
 
 
         foreach (var list in undoStack)
         foreach (var list in undoStack)
         {
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
                 change.Dispose();
         }
         }
 
 
         foreach (var list in redoStack)
         foreach (var list in redoStack)
         {
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
                 change.Dispose();
         }
         }
     }
     }
@@ -77,14 +77,14 @@ public class DocumentChangeTracker : IDisposable
 
 
         foreach (var changesToDispose in redoStack)
         foreach (var changesToDispose in redoStack)
         {
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
                 changeToDispose.Dispose();
         }
         }
 
 
         redoStack.Clear();
         redoStack.Clear();
     }
     }
 
 
-    private void CompletePacket()
+    private void CompletePacket(ActionSource source)
     {
     {
         if (activePacket is null)
         if (activePacket is null)
             return;
             return;
@@ -92,24 +92,25 @@ public class DocumentChangeTracker : IDisposable
         // maybe merge with previous
         // maybe merge with previous
         if (activePacket.Count == 1 &&
         if (activePacket.Count == 1 &&
             undoStack.Count > 0 &&
             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
         else
         {
         {
-            undoStack.Push(activePacket);
+            undoStack.Push((source, activePacket));
         }
         }
 
 
         activePacket = null;
         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 false;
         }
         }
         return true;
         return true;
@@ -125,11 +126,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
             return new List<IChangeInfo>();
         }
         }
         List<IChangeInfo> changeInfos = new();
         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 _) => { },
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -149,11 +150,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
             return new List<IChangeInfo>();
         }
         }
         List<IChangeInfo> changeInfos = new();
         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 _) => { },
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -172,13 +173,13 @@ public class DocumentChangeTracker : IDisposable
         }
         }
         foreach (var changesToDispose in redoStack)
         foreach (var changesToDispose in redoStack)
         {
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
                 changeToDispose.Dispose();
         }
         }
 
 
         foreach (var changesToDispose in undoStack)
         foreach (var changesToDispose in undoStack)
         {
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
                 changeToDispose.Dispose();
         }
         }
 
 
@@ -255,7 +256,7 @@ public class DocumentChangeTracker : IDisposable
         return info;
         return info;
     }
     }
 
 
-    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<IAction> actions)
+    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<(ActionSource, IAction)> actions)
     {
     {
         List<IChangeInfo?> changeInfos = new();
         List<IChangeInfo?> changeInfos = new();
         void AddInfo(OneOf<None, IChangeInfo, List<IChangeInfo>> info) =>
         void AddInfo(OneOf<None, IChangeInfo, List<IChangeInfo>> info) =>
@@ -266,7 +267,7 @@ public class DocumentChangeTracker : IDisposable
 
 
         foreach (var action in actions)
         foreach (var action in actions)
         {
         {
-            switch (action)
+            switch (action.Item2)
             {
             {
                 case IMakeChangeAction act:
                 case IMakeChangeAction act:
                     AddInfo(ProcessMakeChangeAction(act));
                     AddInfo(ProcessMakeChangeAction(act));
@@ -284,7 +285,7 @@ public class DocumentChangeTracker : IDisposable
                     AddInfo(Redo());
                     AddInfo(Redo());
                     break;
                     break;
                 case ChangeBoundary_Action:
                 case ChangeBoundary_Action:
-                    CompletePacket();
+                    CompletePacket(action.Item1);
                     break;
                     break;
                 case DeleteRecordedChanges_Action:
                 case DeleteRecordedChanges_Action:
                     DeleteAllChanges();
                     DeleteAllChanges();
@@ -298,7 +299,7 @@ public class DocumentChangeTracker : IDisposable
         return changeInfos;
         return changeInfos;
     }
     }
 
 
-    public async Task<List<IChangeInfo?>> ProcessActions(IReadOnlyList<IAction> actions)
+    public async Task<List<IChangeInfo?>> ProcessActions(List<(ActionSource, IAction)> actions)
     {
     {
         if (disposed)
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -310,7 +311,7 @@ public class DocumentChangeTracker : IDisposable
         return result;
         return result;
     }
     }
 
 
-    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<IAction> actions)
+    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<(ActionSource, IAction)> actions)
     {
     {
         if (disposed)
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -322,3 +323,9 @@ public class DocumentChangeTracker : IDisposable
         return result;
         return result;
     }
     }
 }
 }
+
+public enum ActionSource
+{
+    User,
+    Automated
+}

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

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

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

@@ -66,7 +66,7 @@ public class DocumentRenderer
 
 
         return toDrawOn;
         return toDrawOn;
     }
     }
-
+    
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
     public OneOf<Chunk, EmptyChunk> RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
         RectI? globalClippingRect = null)
         RectI? globalClippingRect = null)
     {
     {
@@ -125,7 +125,7 @@ public class DocumentRenderer
         HashSet<Guid> layersToCombine, RectI? globalClippingRect)
         HashSet<Guid> layersToCombine, RectI? globalClippingRect)
     {
     {
         using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
         using RenderingContext context = new(frame, chunkPos, resolution, Document.Size);
-        NodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
+        IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         try
         try
         {
         {
             return RenderChunkOnGraph(chunkPos, resolution, globalClippingRect, membersOnlyGraph, context);
             return RenderChunkOnGraph(chunkPos, resolution, globalClippingRect, membersOnlyGraph, context);
@@ -135,6 +135,47 @@ public class DocumentRenderer
             return new EmptyChunk();
             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,
     private static OneOf<Chunk, EmptyChunk> RenderChunkOnGraph(VecI chunkPos, ChunkResolution resolution,
         RectI? globalClippingRect,
         RectI? globalClippingRect,
@@ -187,7 +228,13 @@ public class DocumentRenderer
         return chunk;
         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();
         NodeGraph membersOnlyGraph = new();
 
 
@@ -199,7 +246,7 @@ public class DocumentRenderer
 
 
         fullGraph.TryTraverse(node =>
         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);
                 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 = backend;
-            _current.RenderingServer = server;
+            _current.RenderingDispatcher = server;
             backend.Setup();
             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 IImageFilterImplementation ImageFilterImplementation { get; }
         public IShaderImplementation ShaderImplementation { get; set; }
         public IShaderImplementation ShaderImplementation { get; set; }
         public bool IsHardwareAccelerated { get; }
         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 Concat(in Matrix3X3 first, in Matrix3X3 second);
     public Matrix3X3 PostConcat(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, 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 object GetNativeCanvas(IntPtr objectPointer);
         public void DrawPaint(IntPtr objectPointer, Paint paint);
         public void DrawPaint(IntPtr objectPointer, Paint paint);
         public void DrawImage(IntPtr objectPointer, Image image, int x, int y, 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>
     /// <summary>Gets or sets the z-perspective.</summary>
     /// <value />
     /// <value />
     public float Persp2 { get; set; }
     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)
     public readonly bool Equals(Matrix3X3 obj)
     {
     {
@@ -351,11 +356,21 @@ public struct Matrix3X3 : IEquatable<Matrix3X3>
     {
     {
         return DrawingBackendApi.Current.MatrixImplementation.PostConcat(in this, in globalMatrix);
         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)
     public VecD MapPoint(int p0, int p1)
     {
     {
         return DrawingBackendApi.Current.MatrixImplementation.MapPoint(this, p0, 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)
     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 class Canvas : NativeObject
     {
     {
         public override object Native => DrawingBackendApi.Current.CanvasImplementation.GetNativeCanvas(ObjectPointer);
         public override object Native => DrawingBackendApi.Current.CanvasImplementation.GetNativeCanvas(ObjectPointer);
+        public Matrix3X3 ActiveMatrix => DrawingBackendApi.Current.CanvasImplementation.GetActiveMatrix(ObjectPointer);
+
         public event SurfaceChangedEventHandler? Changed;
         public event SurfaceChangedEventHandler? Changed;
 
 
         public Canvas(IntPtr objPtr) : base(objPtr)
         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);
             canvas.DrawImage(img, x, y, skPaint);
         }
         }
 
 
+        public Matrix3X3 GetActiveMatrix(IntPtr objectPointer)
+        {
+            return ManagedInstances[objectPointer].TotalMatrix.ToMatrix3X3();
+        }
+
         public int Save(IntPtr objPtr)
         public int Save(IntPtr objPtr)
         {
         {
             return ManagedInstances[objPtr].Save();
             return ManagedInstances[objPtr].Save();
@@ -112,9 +117,10 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
 
         public void DrawPoints(IntPtr objPtr, PointMode pointMode, Point[] points, Paint paint)
         public void DrawPoints(IntPtr objPtr, PointMode pointMode, Point[] points, Paint paint)
         {
         {
+            SKPoint[] skPoints = CastUtility.UnsafeArrayCast<Point, SKPoint>(points);
             ManagedInstances[objPtr].DrawPoints(
             ManagedInstances[objPtr].DrawPoints(
                 (SKPointMode)pointMode,
                 (SKPointMode)pointMode,
-                CastUtility.UnsafeArrayCast<Point, SKPoint>(points),
+                skPoints,
                 _paintImpl[paint.ObjectPointer]);
                 _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);
             var mapped = matrix.ToSkMatrix().MapPoint(p0, p1);
             return new VecD(mapped.X, mapped.Y);
             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 bool IsHardwareAccelerated => GraphicsContext != null;
         
         
-        public IRenderingServer RenderingServer { get; set; }
+        public IRenderingServer RenderingDispatcher { get; set; }
 
 
         public IColorImplementation ColorImplementation { get; }
         public IColorImplementation ColorImplementation { get; }
         public IImageImplementation ImageImplementation { 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="NumbersCategoryBackgroundColor">#666666</Color>
             <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
             <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</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>
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
 
 
@@ -163,6 +167,10 @@
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
             <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="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <system:Double x:Key="ControlCornerRadiusValue">5</system:Double>
             <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-swatches">&#xE982;</system:String>
             <system:String x:Key="icon-nodes">&#xe984;</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-onion">&#xe985;</system:String>
+            
+            <system:String x:Key="icon-snapping">&#xfffd;</system:String>
         </ResourceDictionary>
         </ResourceDictionary>
     </Styles.Resources>
     </Styles.Resources>
     
     

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

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

+ 31 - 0
src/PixiEditor.sln

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

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