Przeglądaj źródła

Merge branch 'master' into decodes

flabbet 8 miesięcy temu
rodzic
commit
08a3a6a362
100 zmienionych plików z 1957 dodań i 612 usunięć
  1. 2 1
      README.md
  2. 5 4
      src/ChunkyImageLib/Chunk.cs
  3. 42 21
      src/ChunkyImageLib/ChunkyImage.cs
  4. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  5. 2 2
      src/ChunkyImageLib/DataHolders/ColorBounds.cs
  6. 22 0
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  7. 2 0
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  8. 10 0
      src/ChunkyImageLib/Operations/EllipseCache.cs
  9. 136 7
      src/ChunkyImageLib/Operations/EllipseHelper.cs
  10. 28 16
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  11. 1 1
      src/ChunkyImageLib/Operations/PixelOperation.cs
  12. 2 2
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  13. 14 1
      src/ChunkyImageLib/Operations/ReplaceColorOperation.cs
  14. 1 1
      src/Directory.Build.props
  15. 1 1
      src/Drawie
  16. 1 1
      src/PixiDocks
  17. 12 17
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  18. 1 0
      src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj
  19. 5 2
      src/PixiEditor.Beta/PixiEditor.Beta.csproj
  20. 1 1
      src/PixiEditor.Beta/extension.json
  21. 62 3
      src/PixiEditor.Builder/build/Program.cs
  22. 5 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/ProcessingColorSpace_ChangeInfo.cs
  23. 4 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  24. 4 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  25. 2 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  26. 17 10
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  27. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs
  29. 51 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs
  30. 48 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  31. 3 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IChunkRenderable.cs
  32. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs
  33. 14 9
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  34. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  35. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  36. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyPathData.cs
  37. 41 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  38. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  39. 2 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  40. 59 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  41. 70 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CustomOutputNode.cs
  42. 0 57
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs
  43. 1 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  44. 25 41
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  45. 30 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  46. 1 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  47. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  48. 29 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  49. 68 27
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  50. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  51. 15 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  52. 24 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  53. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  54. 26 19
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  55. 29 17
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  56. 33 24
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  57. 24 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  58. 28 21
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  59. 18 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  60. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  61. 0 14
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  62. 7 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  63. 37 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs
  64. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  65. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  66. 21 3
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs
  67. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs
  68. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFramesStartPos_UpdateableChange.cs
  69. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  70. 191 57
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  71. 8 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs
  72. 40 16
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  73. 14 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  74. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  75. 46 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs
  76. 1 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs
  77. 47 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DuplicateNode_Change.cs
  78. 2 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  79. 65 0
      src/PixiEditor.ChangeableDocument/Changes/Properties/ChangeProcessingColorSpace_Change.cs
  80. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs
  81. 17 0
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  82. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  83. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectEllipse_UpdateableChange.cs
  84. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  85. 139 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  86. 3 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  87. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs
  88. 10 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  89. 173 27
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  90. 6 1
      src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs
  91. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  92. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  93. 1 0
      src/PixiEditor.Extensions/UI/Overlays/IOverlay.cs
  94. 12 0
      src/PixiEditor.SVG/Attributes/SvgValueAttribute.cs
  95. 6 0
      src/PixiEditor.SVG/Elements/SvgCircle.cs
  96. 8 1
      src/PixiEditor.SVG/Elements/SvgEllipse.cs
  97. 11 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  98. 9 2
      src/PixiEditor.SVG/Elements/SvgImage.cs
  99. 7 0
      src/PixiEditor.SVG/Elements/SvgLine.cs
  100. 8 1
      src/PixiEditor.SVG/Elements/SvgMask.cs

+ 2 - 1
README.md

@@ -8,8 +8,9 @@
 [![Downloads](https://img.shields.io/github/downloads/PixiEditor/PixiEditor/total)](https://github.com/flabbet/PixiEditor/releases)
 [![Discord Server](https://badgen.net/badge/discord/join%20chat/7289DA?icon=discord)](https://discord.gg/qSRMYmq)
 [![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/PixiEditor?label=%20r%2FPixiEditor&logoColor=%23e3002d)](https://reddit.com/r/PixiEditor)
+[![Forum](https://img.shields.io/badge/PixiEditor-Forum-red?link=https%3A%2F%2Fforum.pixieditor.net%2F)](https://forum.pixieditor.net/)
 
-### Check out our website [pixieditor.net](https://pixieditor.net)
+### Check out our website [pixieditor.net](https://pixieditor.net) and [PixiEditor Forum](https://forum.pixieditor.net/)
 
 # Contributions temporarily freezed!
 

+ 5 - 4
src/ChunkyImageLib/Chunk.cs

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -48,21 +49,21 @@ public class Chunk : IDisposable
     public bool Disposed => returned;
 
     private Surface internalSurface;
-    private Chunk(ChunkResolution resolution)
+    private Chunk(ChunkResolution resolution, ColorSpace colorSpace)
     {
         int size = resolution.PixelSize();
 
         Resolution = resolution;
         PixelSize = new(size, size);
-        internalSurface = new Surface(PixelSize);
+        internalSurface = new Surface(new ImageInfo(size, size, ColorType.RgbaF16, AlphaType.Premul, colorSpace));
     }
 
     /// <summary>
     /// Tries to take a chunk with the <paramref name="resolution"/> from the pool, or creates a new one
     /// </summary>
-    public static Chunk Create(ChunkResolution resolution = ChunkResolution.Full)
+    public static Chunk Create(ColorSpace chunkCs, ChunkResolution resolution = ChunkResolution.Full)
     {
-        var chunk = ChunkPool.Instance.Get(resolution) ?? new Chunk(resolution);
+        var chunk = ChunkPool.Instance.Get(resolution) ?? new Chunk(resolution, chunkCs);
         chunk.returned = false;
         Interlocked.Increment(ref chunkCounter);
         return chunk;

+ 42 - 21
src/ChunkyImageLib/ChunkyImage.cs

@@ -75,6 +75,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
     private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
 
+    public ColorSpace ProcessingColorSpace { get; set; }
+
     public int CommitCounter => commitCounter;
 
     public VecI CommittedSize { get; private set; }
@@ -103,7 +105,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
 
-    public ChunkyImage(VecI size)
+    public ChunkyImage(VecI size, ColorSpace colorSpace)
     {
         CommittedSize = size;
         LatestSize = size;
@@ -128,9 +130,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             [ChunkResolution.Quarter] = new(),
             [ChunkResolution.Eighth] = new(),
         };
+
+        ProcessingColorSpace = colorSpace;
     }
 
-    public ChunkyImage(Surface image) : this(image.Size)
+    public ChunkyImage(Surface image, ColorSpace colorSpace) : this(image.Size, colorSpace)
     {
         EnqueueDrawImage(VecI.Zero, image);
         CommitChanges();
@@ -250,7 +254,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
-            ChunkyImage output = new(LatestSize);
+            ChunkyImage output = new(LatestSize, ProcessingColorSpace);
             var chunks = FindCommittedChunks();
             foreach (var chunk in chunks)
             {
@@ -276,7 +280,23 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
             {
                 null => Colors.Transparent,
-                var chunk => chunk.Surface.GetSRGBPixel(posInChunk)
+                var chunk => chunk.Surface.GetSrgbPixel(posInChunk)
+            };
+        }
+    }
+
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public Color GetCommittedPixelRaw(VecI posOnImage)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            var chunkPos = OperationHelper.GetChunkPos(posOnImage, FullChunkSize);
+            var posInChunk = posOnImage - chunkPos * FullChunkSize;
+            return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) switch
+            {
+                null => Colors.Transparent,
+                var chunk => chunk.Surface.GetRawPixel(posInChunk)
             };
         }
     }
@@ -297,7 +317,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return committedChunk switch
                 {
                     null => Colors.Transparent,
-                    _ => committedChunk.Surface.GetSRGBPixel(posInChunk)
+                    _ => committedChunk.Surface.GetSrgbPixel(posInChunk)
                 };
             }
 
@@ -308,7 +328,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return latestChunk switch
                 {
                     null => Colors.Transparent,
-                    _ => latestChunk.Surface.GetSRGBPixel(posInChunk)
+                    _ => latestChunk.Surface.GetSrgbPixel(posInChunk)
                 };
             }
 
@@ -318,18 +338,18 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Color committedColor = committedChunk is null
                     ? Colors.Transparent
-                    : committedChunk.Surface.GetSRGBPixel(posInChunk);
+                    : committedChunk.Surface.GetSrgbPixel(posInChunk);
                 Color latestColor = latestChunk is null
                     ? Colors.Transparent
-                    : latestChunk.Surface.GetSRGBPixel(posInChunk);
+                    : latestChunk.Surface.GetSrgbPixel(posInChunk);
                 // using a whole chunk just to draw 1 pixel is kinda dumb,
                 // but this should be faster than any approach that requires allocations
-                using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
+                using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
                 using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
                 using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
                 tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
                 tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
-                return tempChunk.Surface.GetSRGBPixel(VecI.Zero);
+                return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
             }
         }
     }
@@ -381,7 +401,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             }
 
             // combine with committed and then draw
-            using var tempChunk = Chunk.Create(resolution);
+            using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
             tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
                 ReplacingPaint);
             blendModePaint.BlendMode = blendMode;
@@ -597,7 +617,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
-            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased, paint);
+            EllipseOperation operation = new(location, strokeColor, fillColor, strokeWidth, rotationRad, antiAliased,
+                paint);
             EnqueueOperation(operation);
         }
     }
@@ -1022,7 +1043,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                     blendModePaint.BlendMode = blendMode;
                     if (lockTransparency)
                     {
-                        using Chunk tempChunk = Chunk.Create(resolution);
+                        using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
                         tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0,
                             ReplacingPaint);
                         maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
@@ -1197,7 +1218,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             return new FilledChunk();
         }
 
-        var intersection = Chunk.Create(resolution);
+        var intersection = Chunk.Create(ProcessingColorSpace, resolution);
         intersection.Surface.DrawingSurface.Canvas.Clear(Colors.White);
 
         foreach (var mask in activeClips)
@@ -1250,7 +1271,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             // drawing with raster clipping
             var clip = combinedRasterClips.AsT2;
 
-            using var tempChunk = Chunk.Create(targetChunk.Resolution);
+            using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
             targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
 
             CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
@@ -1365,7 +1386,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         // for full res chunks: nothing exists, create brand new chunk
         if (resolution == ChunkResolution.Full)
         {
-            var newChunk = Chunk.Create(resolution);
+            var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
             committedChunks[resolution][chunkPos] = newChunk;
             return newChunk;
         }
@@ -1374,7 +1395,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         Chunk? existingFullResChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
         if (existingFullResChunk is not null)
         {
-            var newChunk = Chunk.Create(resolution);
+            var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
             newChunk.Surface.DrawingSurface.Canvas.Save();
             newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
 
@@ -1388,7 +1409,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         // for low res chunks: full res version doesn't exist
         {
             GetOrCreateCommittedChunk(chunkPos, ChunkResolution.Full);
-            var newChunk = Chunk.Create(resolution);
+            var newChunk = Chunk.Create(ProcessingColorSpace, resolution);
             committedChunks[resolution][chunkPos] = newChunk;
             return newChunk;
         }
@@ -1408,7 +1429,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         var maybeCommittedAnyRes = MaybeGetCommittedChunk(chunkPos, resolution);
         if (maybeCommittedAnyRes is not null)
         {
-            Chunk newChunk = Chunk.Create(resolution);
+            Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
             if (blendMode == BlendMode.Src)
                 maybeCommittedAnyRes.Surface.CopyTo(newChunk.Surface);
             else
@@ -1424,14 +1445,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             //create low res committed chunk
             var committedChunkLowRes = GetOrCreateCommittedChunk(chunkPos, resolution);
             //create latest based on it
-            Chunk newChunk = Chunk.Create(resolution);
+            Chunk newChunk = Chunk.Create(ProcessingColorSpace, resolution);
             committedChunkLowRes.Surface.CopyTo(newChunk.Surface);
             latestChunks[resolution][chunkPos] = newChunk;
             return newChunk;
         }
 
         // no previous chunks exist
-        var newLatestChunk = Chunk.Create(resolution);
+        var newLatestChunk = Chunk.Create(ProcessingColorSpace, resolution);
         newLatestChunk.Surface.DrawingSurface.Canvas.Clear();
         latestChunks[resolution][chunkPos] = newLatestChunk;
         return newLatestChunk;

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -16,7 +16,7 @@ public class CommittedChunkStorage : IDisposable
     {
         foreach (var chunkPos in committedChunksToSave)
         {
-            Chunk copy = Chunk.Create();
+            Chunk copy = Chunk.Create(image.ProcessingColorSpace);
             if (!image.DrawCommittedChunkOn(chunkPos, ChunkResolution.Full, copy.Surface.DrawingSurface, VecI.Zero, ReplacingPaint))
             {
                 copy.Dispose();

+ 2 - 2
src/ChunkyImageLib/DataHolders/ColorBounds.cs

@@ -25,8 +25,8 @@ public struct ColorBounds
     {
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         {
-            float subHalf = channel > 0 ? channel - .5f : channel;
-            float addHalf = channel < 255 ? channel + .5f : channel;
+            float subHalf = channel > 0 ? channel - 1f : channel;
+            float addHalf = channel < 255 ? channel + 1f : channel;
             
             var lower = subHalf * alpha / 255f;
             var upper = addHalf * alpha / 255f;

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

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

+ 2 - 0
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -22,4 +23,5 @@ public interface IReadOnlyChunkyImage
     HashSet<VecI> FindAllChunks();
     VecI CommittedSize { get; }
     VecI LatestSize { get; }
+    public ColorSpace ProcessingColorSpace { get; }
 }

+ 10 - 0
src/ChunkyImageLib/Operations/EllipseCache.cs

@@ -0,0 +1,10 @@
+using System.Collections.Concurrent;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+
+namespace ChunkyImageLib.Operations;
+
+public static class EllipseCache
+{
+    public static readonly ConcurrentDictionary<VecI, VectorPath> Ellipses = new();
+}

+ 136 - 7
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -106,7 +106,7 @@ public class EllipseHelper
         float radiusY = (rect.Height - 1) / 2.0f;
         if (rotationRad == 0)
             return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
-        
+
         return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
     }
 
@@ -185,6 +185,130 @@ public class EllipseHelper
         return listToFill;
     }
 
+    /// <summary>
+    ///     Constructs pixel-perfect ellipse outline represented as a vector path.
+    ///  This function is quite heavy, for less precise but faster results use <see cref="GenerateEllipseVectorFromRect"/>.
+    /// </summary>
+    /// <param name="rectangle">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an ellipse outline.</returns>
+    public static VectorPath ConstructEllipseOutline(RectI rectangle)
+    {
+        if (EllipseCache.Ellipses.TryGetValue(rectangle.Size, out var cachedPath))
+        {
+            VectorPath finalPath = new(cachedPath);
+            finalPath.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
+            
+            return finalPath;
+        }
+        
+        if (rectangle.Width < 3 || rectangle.Height < 3)
+        {
+            VectorPath rectPath = new();
+            rectPath.AddRect((RectD)rectangle);
+
+            return rectPath;
+        }
+
+        if (rectangle is { Width: 3, Height: 3 })
+        {
+            return CreateThreePixelCircle((VecI)rectangle.Center);
+        }
+
+        var center = rectangle.Size / 2d;
+        RectI rect = new RectI(0, 0, rectangle.Width, rectangle.Height);
+        var points = GenerateEllipseFromRect(rect, 0).ToList();
+        points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
+        List<VecI> finalPoints = new();
+        for (int i = 0; i < points.Count; i++)
+        {
+            VecI prev = points[Mod(i - 1, points.Count)];
+            VecI point = points[i];
+            VecI next = points[Mod(i + 1, points.Count)];
+
+            bool atBottom = point.Y >= center.Y;
+            bool onRight = point.X >= center.X;
+            if (atBottom)
+            {
+                if (onRight)
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                    finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    finalPoints.Add(new(point.X, point.Y + 1));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(point);
+                }
+            }
+            else
+            {
+                if (onRight)
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(point);
+                    finalPoints.Add(new(point.X + 1, point.Y));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                    finalPoints.Add(point);
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                }
+            }
+        }
+
+        VectorPath path = new();
+
+        path.MoveTo(new VecF(finalPoints[0].X, finalPoints[0].Y));
+        for (var index = 1; index < finalPoints.Count; index++)
+        {
+            var point = finalPoints[index];
+            path.LineTo(new VecF(point.X, point.Y));
+        }
+
+        path.Close();
+        
+        EllipseCache.Ellipses[rectangle.Size] = new VectorPath(path);
+        
+        path.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
+        return path;
+    }
+
+    public static VectorPath CreateThreePixelCircle(VecI rectanglePos)
+    {
+        var path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(0, -1));
+        path.LineTo(new VecF(1, -1));
+        path.LineTo(new VecF(1, 0));
+        path.LineTo(new VecF(2, 0));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(1, 1));
+        path.LineTo(new VecF(1, 2));
+        path.LineTo(new VecF(0, 2));
+        path.LineTo(new VecF(0, 1));
+        path.LineTo(new VecF(-1, 1));
+        path.LineTo(new VecF(-1, 0));
+        path.Close();
+        
+        path.Transform(Matrix3X3.CreateTranslation(rectanglePos.X, rectanglePos.Y));
+        
+        return path;
+    }
+
+    private static int Mod(int x, int m) => (x % m + m) % m;
+
     // 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.
@@ -203,7 +327,7 @@ public class EllipseHelper
 
         // less than, because y grows downwards
         //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
-        
+
         //rotationRad = double.Round(rotationRad, 1);
 
         double currentTetha = 0;
@@ -221,13 +345,13 @@ public class EllipseHelper
 
             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 (!listToFill.Add(floored)) return;
 
         if (lastPoints[0] == default)
         {
@@ -247,7 +371,7 @@ public class EllipseHelper
 
             lastPoints[0] = floored;
             lastPoints[1] = default;
-            
+
             return;
         }
 
@@ -345,13 +469,18 @@ public class EllipseHelper
         }
     }
 
+    /// <summary>
+    ///     This function generates a vector path that represents an oval. For pixel-perfect circle use <see cref="ConstructEllipseOutline"/>.
+    /// </summary>
+    /// <param name="location">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an oval.</returns>
     public static VectorPath GenerateEllipseVectorFromRect(RectD location)
     {
         VectorPath path = new();
         path.AddOval(location);
-       
+
         path.Close();
-        
+
         return path;
     }
 }

+ 28 - 16
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -46,14 +46,22 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
             if (Math.Abs(rotation) < 0.001)
             {
-                var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
+                if (strokeWidth == 0)
+                {
+                    ellipseOutline = EllipseHelper.ConstructEllipseOutline((RectI)location);
+                }
+                else
+                {
+                    var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
 
-                ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
+                    ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
 
-                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
-                {
-                    (var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
-                    ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                    {
+                        (var fill, ellipseFillRect) =
+                            EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
+                        ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    }
                 }
             }
             else
@@ -98,7 +106,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         paint.IsAntiAliased = false;
         if (strokeWidth - 1 < 0.01)
         {
-            if (Math.Abs(rotation) < 0.001)
+            if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
                 if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
                 {
@@ -107,7 +115,7 @@ internal class EllipseOperation : IMirroredDrawOperation
                     surf.Canvas.DrawRect((RectD)ellipseFillRect!.Value, paint);
                 }
                 
-                paint.Color = strokeColor;
+                paint.Color = strokeWidth <= 0 ? fillColor : strokeColor;
                 paint.StrokeWidth = 1f;
                 surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
             }
@@ -122,12 +130,15 @@ internal class EllipseOperation : IMirroredDrawOperation
                     paint.Style = PaintStyle.Fill;
                     surf.Canvas.DrawPath(ellipseOutline!, paint);
                 }
-                
-                paint.Color = strokeColor;
-                paint.Style = PaintStyle.Stroke;
-                paint.StrokeWidth = 1f;
-                
-                surf.Canvas.DrawPath(ellipseOutline!, paint);
+
+                if (strokeWidth > 0)
+                {
+                    paint.Color = strokeColor;
+                    paint.Style = PaintStyle.Stroke;
+                    paint.StrokeWidth = 1;
+
+                    surf.Canvas.DrawPath(ellipseOutline!, paint);
+                }
 
                 surf.Canvas.Restore();
             }
@@ -165,9 +176,9 @@ internal class EllipseOperation : IMirroredDrawOperation
         surf.Canvas.DrawOval(fillRect.Center, fillRect.Size / 2f, paint);
         
         paint.IsAntiAliased = true;
-        paint.Color = strokeColor;
+        paint.Color = strokeWidth <= 0 ? fillColor : strokeColor;
         paint.Style = PaintStyle.Stroke;
-        paint.StrokeWidth = strokeWidth;
+        paint.StrokeWidth = strokeWidth <= 0 ? 1f : strokeWidth;
         
         RectD strokeRect = ((RectD)location).Inflate((-strokeWidth / 2f));
         
@@ -207,5 +218,6 @@ internal class EllipseOperation : IMirroredDrawOperation
         paint.Dispose();
         outerPath?.Dispose();
         innerPath?.Dispose();
+        ellipseOutline?.Dispose();
     }
 }

+ 1 - 1
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -55,7 +55,7 @@ internal class PixelOperation : IMirroredDrawOperation
         if (colorProcessor != null && getCommitedPixelFunc != null)
         {
             var pos = pixel - chunkPos * ChunkyImage.FullChunkSize;
-            pixelColor = colorProcessor(getCommitedPixelFunc(pixel), chunk.Surface.GetSRGBPixel(pos));
+            pixelColor = colorProcessor(getCommitedPixelFunc(pixel), chunk.Surface.GetSrgbPixel(pos));
         }
 
         return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));

+ 2 - 2
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -86,8 +86,8 @@ internal class RectangleOperation : IMirroredDrawOperation
 
         // draw stroke
         surf.Canvas.Save();
-        paint.StrokeWidth = (float)Data.StrokeWidth;
-        paint.Color = Data.StrokeColor;
+        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
+        paint.Color = Data.StrokeWidth > 0 ? Data.StrokeColor : Data.FillColor;
         paint.Style = PaintStyle.Stroke;
         RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
         surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);

+ 14 - 1
src/ChunkyImageLib/Operations/ReplaceColorOperation.cs

@@ -2,9 +2,11 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 internal class ReplaceColorOperation : IDrawOperation
 {
     private readonly Color oldColor;
@@ -25,7 +27,18 @@ internal class ReplaceColorOperation : IDrawOperation
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        ReplaceColor(oldColorBounds, newColorBits, targetChunk);
+        ulong targetColorBits = newColor.ToULong();
+        ColorBounds colorBounds = new(oldColor);
+        if (targetChunk.Surface.ImageInfo.ColorSpace is { IsSrgb: false })
+        {
+            var transform = ColorSpace.CreateSrgb().GetTransformFunction();
+            targetColorBits = newColor.TransformColor(transform).ToULong();
+
+            var transformOld = targetChunk.Surface.ImageInfo.ColorSpace.GetTransformFunction();
+            colorBounds = new ColorBounds(oldColor.TransformColor(transform));
+        }
+
+        ReplaceColor(colorBounds, targetColorBits, targetChunk);
     }
 
     private static unsafe void ReplaceColor(ColorBounds oldColorBounds, ulong newColorBits, Chunk chunk)

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.2.2</AvaloniaVersion>
+		    <AvaloniaVersion>11.2.3</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 512f280da10f3fbf0552f2bd79d1e1b54385813d
+Subproject commit e1cab4d5a37165457960f13d2b39041f8ce66722

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 68dafe4747a6cead83359ede8e0bbc65cdda02a8
+Subproject commit 5f14bdf0e46dd470e46a88ce5f58de4e02c68e94

+ 12 - 17
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -7,6 +7,7 @@ using FFMpegCore.Pipes;
 using PixiEditor.AnimationRenderer.Core;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.OperatingSystem;
 
 namespace PixiEditor.AnimationRenderer.FFmpeg;
 
@@ -16,16 +17,10 @@ public class FFMpegRenderer : IAnimationRenderer
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
 
-    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback = null)
+    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
+        Action<double>? progressCallback = null)
     {
-        string path = "ThirdParty/{0}/ffmpeg";
-#if WINDOWS
-        path = string.Format(path, "Windows");
-#elif MACOS
-        path = string.Format(path, "MacOS");
-#elif LINUX
-        path = string.Format(path, "Linux");
-#endif
+        string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
         GlobalFFOptions.Configure(new FFOptions()
         {
@@ -35,26 +30,26 @@ public class FFMpegRenderer : IAnimationRenderer
         try
         {
             List<ImgFrame> frames = new();
-            
+
             foreach (var frame in rawFrames)
             {
                 frames.Add(new ImgFrame(frame));
             }
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
-            
+
             string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
-            
+
             if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
             {
                 Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
             }
-            
+
             if (RequiresPaletteGeneration())
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);
             }
-            
+
             streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
             var args = FFMpegArguments
@@ -67,15 +62,15 @@ public class FFMpegRenderer : IAnimationRenderer
             TimeSpan totalTimeSpan = TimeSpan.FromSeconds(frames.Count / (float)FrameRate);
             var result = await outputArgs.CancellableThrough(cancellationToken)
                 .NotifyOnProgress(progressCallback, totalTimeSpan).ProcessAsynchronously();
-            
+
             if (RequiresPaletteGeneration())
             {
                 File.Delete(paletteTempPath);
                 Directory.Delete(Path.GetDirectoryName(paletteTempPath));
             }
-            
+
             DisposeStream(frames);
-            
+
             return result;
         }
         catch (Exception e)

+ 1 - 0
src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj

@@ -10,6 +10,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj" />
+      <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
     </ItemGroup>
 
     <ItemGroup>

+ 5 - 2
src/PixiEditor.Beta/PixiEditor.Beta.csproj

@@ -8,9 +8,12 @@
     <Nullable>disable</Nullable>
     <PublishTrimmed>true</PublishTrimmed>
     <WasmSingleFileBundle>true</WasmSingleFileBundle>
+    <EventSourceSupport>false</EventSourceSupport>
+    <UseSystemResourceKeys>true</UseSystemResourceKeys>
+    <EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
+    <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
+    <DebuggerSupport>false</DebuggerSupport>
     <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
-    <!--TODO: Temp solution, make it properly build by build system and copy to target dir on publish-->
-    <PixiExtOutputPath>$(MSBuildProjectDirectory)\..\PixiEditor\Extensions</PixiExtOutputPath>
     <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
     <RootNamespace>PixiEditor.Beta</RootNamespace>
   </PropertyGroup>

+ 1 - 1
src/PixiEditor.Beta/extension.json

@@ -2,7 +2,7 @@
   "displayName": "PixiEditor Beta",
   "uniqueName": "PixiEditor.Beta",
   "description": "Open Beta of PixiEditor 2.0",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "author": {
     "name": "PixiEditor",
     "email": "[email protected]",

+ 62 - 3
src/PixiEditor.Builder/build/Program.cs

@@ -23,6 +23,8 @@ public class BuildContext : FrostingContext
 {
     public string PathToProject { get; set; } = "../PixiEditor/PixiEditor.csproj";
 
+    public string[] ExtensionProjectsToInclude { get; set; } = [];
+
     public string CrashReportWebhookUrl { get; set; }
 
     public string AnalyticsUrl { get; set; }
@@ -34,7 +36,7 @@ public class BuildContext : FrostingContext
     public string OutputDirectory { get; set; } = "Builds";
 
     public bool SelfContained { get; set; } = false;
-    
+
     public string Runtime { get; set; }
 
     public BuildContext(ICakeContext context)
@@ -49,6 +51,12 @@ public class BuildContext : FrostingContext
             PathToProject = context.Arguments.GetArgument("project-path");
         }
 
+        bool hasCustomExtensionProjects = context.Arguments.HasArgument("extension-projects");
+        if (hasCustomExtensionProjects)
+        {
+            ExtensionProjectsToInclude = context.Arguments.GetArgument("extension-projects").Split(';');
+        }
+
         bool hasCustomConfiguration = context.Arguments.HasArgument("build-configuration");
         if (hasCustomConfiguration)
         {
@@ -60,7 +68,7 @@ public class BuildContext : FrostingContext
         {
             OutputDirectory = context.Arguments.GetArgument("o");
         }
-        
+
         bool hasSelfContained = context.Arguments.HasArgument("self-contained");
         if (hasSelfContained)
         {
@@ -80,7 +88,7 @@ public class BuildContext : FrostingContext
 }
 
 [TaskName("Default")]
-[IsDependentOn(typeof(BuildProjectTask))]
+[IsDependentOn(typeof(CopyExtensionsTask))]
 public sealed class DefaultTask : FrostingTask<BuildContext>
 {
     public override void Run(BuildContext context)
@@ -144,3 +152,54 @@ public sealed class BuildProjectTask : FrostingTask<BuildContext>
         File.WriteAllText(constantsPath, context.BackedUpConstants);
     }
 }
+
+[TaskName("BuildExtensions")]
+[IsDependentOn(typeof(BuildProjectTask))]
+public sealed class BuildExtensionsTask : FrostingTask<BuildContext>
+{
+    public override void Run(BuildContext context)
+    {
+        context.Log.Information("Building extensions...");
+        foreach (var project in context.ExtensionProjectsToInclude)
+        {
+            var settings = new DotNetPublishSettings() { Configuration = context.BuildConfiguration, };
+
+            context.DotNetPublish(project, settings);
+        }
+    }
+}
+
+[TaskName("CopyExtensions")]
+[IsDependentOn(typeof(BuildExtensionsTask))]
+public sealed class CopyExtensionsTask : FrostingTask<BuildContext>
+{
+    public override void Run(BuildContext context)
+    {
+        context.Log.Information("Copying extensions...");
+        foreach (var project in context.ExtensionProjectsToInclude)
+        {
+            string outputDir = Path.Combine(context.OutputDirectory, "Extensions");
+            string sourceDir = Path.Combine(project, "bin",
+                context.BuildConfiguration, "wasi-wasm", "Extensions");
+
+            CopyDirectoryContents(sourceDir, outputDir, context);
+        }
+    }
+
+    private void CopyDirectoryContents(string sourceDir, string targetDir, BuildContext context)
+    {
+        if (!Directory.Exists(targetDir))
+        {
+            Directory.CreateDirectory(targetDir);
+        }
+
+        context.Log.Information($"Copying contents of {sourceDir} to {targetDir}");
+
+        foreach (var file in Directory.GetFiles(sourceDir))
+        {
+            string targetFile = Path.Combine(targetDir, Path.GetFileName(file));
+            context.Log.Information($"Copying {file} to {targetFile}");
+            File.Copy(file, targetFile, true);
+        }
+    }
+}

+ 5 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Properties/ProcessingColorSpace_ChangeInfo.cs

@@ -0,0 +1,5 @@
+using Drawie.Backend.Core.Surfaces.ImageData;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+
+public record ProcessingColorSpace_ChangeInfo(ColorSpace NewColorSpace) : IChangeInfo;

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Enums;
@@ -19,9 +20,10 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         ImmutableArray<NodePropertyInfo> Inputs,
         ImmutableArray<NodePropertyInfo> Outputs,
+        VecD position,
         NodeMetadata metadata
     ) : base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-        maskIsVisible, Inputs, Outputs, metadata)
+        maskIsVisible, Inputs, Outputs, position, metadata)
     {
     }
 
@@ -38,6 +40,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
             folder.EmbeddedMask is not null,
             folder.MaskIsVisible.Value, CreatePropertyInfos(folder.InputProperties, true, folder.Id),
             CreatePropertyInfos(folder.OutputProperties, false, folder.Id),
+            folder.Position,
             new NodeMetadata(folder));
     }
 }

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -1,4 +1,5 @@
 using System.Collections.Immutable;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -22,9 +23,10 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
         ImmutableArray<NodePropertyInfo> outputs,
+        VecD position,
         NodeMetadata metadata) :
         base(internalName, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
-            maskIsVisible, inputs, outputs, metadata)
+            maskIsVisible, inputs, outputs, position, metadata)
     {
         LockTransparency = lockTransparency;
     }
@@ -46,6 +48,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
             layer is ITransparencyLockable { LockTransparency: true },
             CreatePropertyInfos(layer.InputProperties, true, layer.Id),
             CreatePropertyInfos(layer.OutputProperties, false, layer.Id),
+            layer.Position,
             new NodeMetadata(layer.GetType())
         );
     }

+ 2 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs

@@ -18,8 +18,9 @@ public abstract record class CreateStructureMember_ChangeInfo(
     bool MaskIsVisible,
     ImmutableArray<NodePropertyInfo> InputProperties,
     ImmutableArray<NodePropertyInfo> OutputProperties,
+    VecD position,
     NodeMetadata Metadata
-) : CreateNode_ChangeInfo(InternalName, Name, new VecD(0, 0), Id, InputProperties, OutputProperties, Metadata)
+) : CreateNode_ChangeInfo(InternalName, Name, position, Id, InputProperties, OutputProperties, Metadata)
 {
     public ImmutableArray<NodePropertyInfo> InputProperties { get; init; } = InputProperties;
     public ImmutableArray<NodePropertyInfo> OutputProperties { get; init; } = OutputProperties;

+ 17 - 10
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -32,6 +32,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     IReadOnlyReferenceLayer? IReadOnlyDocument.ReferenceLayer => ReferenceLayer;
     public DocumentRenderer Renderer { get; }
+    public ColorSpace ProcessingColorSpace { get; internal set; } = ColorSpace.CreateSrgbLinear();
 
     /// <summary>
     /// The default size for a new document
@@ -89,13 +90,13 @@ internal class Document : IChangeable, IReadOnlyDocument
         using var paint = new Paint();
 
         Surface image;
-        
+
         if (layer is IReadOnlyImageNode imageNode)
         {
             var chunkyImage = imageNode.GetLayerImageAtFrame(frame);
             using Surface chunkSurface = new Surface(chunkyImage.CommittedSize);
             chunkyImage.DrawCommittedRegionOn(
-                new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y), 
+                new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 ChunkResolution.Full,
                 chunkSurface.DrawingSurface,
                 VecI.Zero);
@@ -108,7 +109,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             /*TODO: this*/
             // image = new Surface(layer.Execute(new RenderingContext(frame, Size)));
         }
-        
+
         //todo: idk if it's correct
         surface.DrawingSurface.Canvas.DrawSurface(image.DrawingSurface, 0, 0, paint);
 
@@ -138,6 +139,11 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// </summary>
     public void ForEveryMember(Action<StructureNode> action) => ForEveryMember(NodeGraph, action);
 
+    public void InitProcessingColorSpace(ColorSpace processingColorSpace)
+    {
+        ProcessingColorSpace = processingColorSpace;
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>
@@ -173,7 +179,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     {
         return NodeGraph.Nodes.Any(x => x.Id == id);
     }
-    
+
     /// <summary>
     ///     Checks if a node in NodeGraph with the given <paramref name="id"/> exists and is of type <typeparamref name="T"/>.
     /// </summary>
@@ -192,7 +198,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// <returns>True if the member can be found, otherwise false</returns>
     public bool HasMember(Guid guid)
     {
-        return HasNode<StructureNode>(guid); 
+        return HasNode<StructureNode>(guid);
     }
 
     /// <summary>
@@ -247,7 +253,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     {
         return NodeGraph.Nodes.FirstOrDefault(x => x.Id == guid);
     }
-    
+
     IReadOnlyNode IReadOnlyDocument.FindNode(Guid guid) => FindNodeOrThrow<Node>(guid);
 
     public T? FindNode<T>(Guid guid) where T : Node
@@ -277,7 +283,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     public bool TryFindMember(Guid guid, [NotNullWhen(true)] out StructureNode? member)
     {
         member = FindMember(guid);
-        return member != null; 
+        return member != null;
     }
 
     /// <summary>
@@ -341,13 +347,13 @@ internal class Document : IChangeable, IReadOnlyDocument
         if (NodeGraph.OutputNode == null) return [];
 
         var list = new List<Node>();
-        
+
         var targetNode = FindNode(guid);
         if (targetNode == null)
         {
             return [];
         }
-        
+
         FillNodePath(targetNode, list);
         return list;
     }
@@ -366,6 +372,7 @@ internal class Document : IChangeable, IReadOnlyDocument
         {
             return [];
         }
+
         FillNodePath<StructureNode>(targetNode, list);
         return list.ToList();
     }
@@ -381,7 +388,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
             return true;
         });
-        
+
         return true;
     }
 

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs

@@ -55,9 +55,9 @@ public class FuncContext
         SamplePosition = Builder.ConstructFloat2(OriginalPosition.X, OriginalPosition.Y);
     }
 
-    public Half4 SampleSurface(DrawingSurface surface, Expression pos)
+    public Half4 SampleSurface(DrawingSurface surface, Expression pos, ColorSampleMode sampleMode)
     {
-        SurfaceSampler texName = Builder.AddOrGetSurface(surface);
+        SurfaceSampler texName = Builder.AddOrGetSurface(surface, sampleMode);
         return Builder.Sample(texName, pos);
     }
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs

@@ -7,6 +7,6 @@ public class ImageLayerNodeFactory : NodeFactory<ImageLayerNode>
 {
     public override ImageLayerNode CreateNode(IReadOnlyDocument document)
     {
-        return new ImageLayerNode(document.Size);
+        return new ImageLayerNode(document.Size, document.ProcessingColorSpace);
     }
 }

+ 51 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/FuncInputProperty.cs

@@ -11,8 +11,10 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncInputProperty
 {
     private T? constantNonOverrideValue;
-    
-    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, null)
+    private int lastConstantHashCode;
+
+    internal FuncInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node,
+        internalName, displayName, null)
     {
         constantNonOverrideValue = defaultValue;
         NonOverridenValue = _ => constantNonOverrideValue;
@@ -28,7 +30,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
                 shaderExpressionVariable.SetConstantValue(toReturn, ConversionTable.Convert);
                 return (T)(object)shaderExpressionVariable;
             }
-            
+
             return (T)toReturn;
         };
         return func;
@@ -40,12 +42,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
         {
             Type targetType = typeof(T);
             bool isShaderExpression = false;
-            if(typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
+            if (typeof(T).IsAssignableTo(typeof(ShaderExpressionVariable)))
             {
                 targetType = targetType.BaseType.GenericTypeArguments[0];
                 isShaderExpression = true;
             }
-            
+
             var sourceObj = delegateToCast.DynamicInvoke(f);
             ConversionTable.TryConvert(sourceObj, targetType, out var result);
             if (isShaderExpression)
@@ -67,12 +69,12 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
                 return (T)toReturn;
             }
-            
-            return result == null ? default : (T)result; 
+
+            return result == null ? default : (T)result;
         };
         return func;
     }
-    
+
     private Expression Adjust(Expression expression, object toReturn, out bool adjustNestedVariables)
     {
         adjustNestedVariables = false;
@@ -89,7 +91,7 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         return expression;
     }
-    
+
     private void AdjustNested(IMultiValueVariable toReturn, Expression expression)
     {
         if (toReturn is not ShaderExpressionVariable shaderExpressionVariable)
@@ -133,8 +135,8 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
             shaderExpressionVariable.SetConstantValue(value, ConversionTable.Convert);
             return;
         }
-        
-        if(ConversionTable.TryConvert(value, typeof(T), out var result))
+
+        if (ConversionTable.TryConvert(value, typeof(T), out var result))
         {
             constantNonOverrideValue = (T)result;
             return;
@@ -142,4 +144,42 @@ public class FuncInputProperty<T> : InputProperty<Func<FuncContext, T>>, IFuncIn
 
         constantNonOverrideValue = default;
     }
+
+    internal override bool CacheChanged
+    {
+        get
+        {
+            if (constantNonOverrideValue == null)
+            {
+                return base.CacheChanged;
+            }
+
+            if (Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+
+            if (Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+
+            if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+            {
+                return expressionVariable.ConstantValueString.GetHashCode() != lastConstantHashCode;
+            }
+
+            return base.CacheChanged;
+        }
+    }
+
+    internal override void UpdateCache()
+    {
+        base.UpdateCache();
+        if (constantNonOverrideValue is ShaderExpressionVariable expressionVariable)
+        {
+            lastConstantHashCode = expressionVariable.ConstantValueString.GetHashCode();
+        }
+    }
 }

+ 48 - 18
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -11,8 +11,12 @@ public class InputProperty : IInputProperty
 {
     private object _internalValue;
     private int _lastExecuteHash = -1;
+    protected int lastConnectionHash = -1;
     private PropertyValidator? validator;
-    
+    private IOutputProperty? connection;
+
+    public event Action ConnectionChanged;
+
     public string InternalPropertyName { get; }
     public string DisplayName { get; }
 
@@ -26,7 +30,7 @@ public class InputProperty : IInputProperty
             }
 
             var connectionValue = Connection.Value;
-            
+
             if (!ValueType.IsAssignableTo(typeof(Delegate)) && connectionValue is Delegate connectionField)
             {
                 return connectionField.DynamicInvoke(FuncContext.NoContext);
@@ -40,7 +44,7 @@ public class InputProperty : IInputProperty
             return connectionValue;
         }
     }
-    
+
     public object NonOverridenValue
     {
         get => _internalValue;
@@ -49,7 +53,7 @@ public class InputProperty : IInputProperty
             _internalValue = value;
         }
     }
-    
+
     public PropertyValidator Validator
     {
         get
@@ -73,28 +77,42 @@ public class InputProperty : IInputProperty
     {
         Func<FuncContext, object> func = f =>
         {
-            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result) ? result : null;
+            return ConversionTable.TryConvert(delegateToCast.DynamicInvoke(f), ValueType, out object result)
+                ? result
+                : null;
         };
         return func;
     }
 
     public Node Node { get; }
-    public Type ValueType { get; } 
-    internal bool CacheChanged
+    public Type ValueType { get; }
+
+    internal virtual bool CacheChanged
     {
         get
         {
+            if(Connection == null && lastConnectionHash != -1)
+            {
+                return true;
+            }
+            
+            if(Connection != null && lastConnectionHash != Connection.GetHashCode())
+            {
+                lastConnectionHash = Connection.GetHashCode();
+                return true;
+            }
+            
             if (Value is ICacheable cacheable)
             {
                 return cacheable.GetCacheHash() != _lastExecuteHash;
             }
 
-            if(Value is null)
+            if (Value is null)
             {
                 return _lastExecuteHash != 0;
             }
-            
-            if(Value.GetType().IsValueType || Value.GetType() == typeof(string))
+
+            if (Value.GetType().IsValueType || Value.GetType() == typeof(string))
             {
                 return Value.GetHashCode() != _lastExecuteHash;
             }
@@ -103,7 +121,7 @@ public class InputProperty : IInputProperty
         }
     }
 
-    internal void UpdateCache()
+    internal virtual void UpdateCache()
     {
         if (Value is null)
         {
@@ -117,12 +135,25 @@ public class InputProperty : IInputProperty
         {
             _lastExecuteHash = Value.GetHashCode();
         }
+        
+        lastConnectionHash = Connection?.GetHashCode() ?? -1;
     }
-    
+
     IReadOnlyNode INodeProperty.Node => Node;
-    
-    public IOutputProperty? Connection { get; set; }
-    
+
+    public IOutputProperty? Connection
+    {
+        get => connection;
+        set
+        {
+            if (connection != value)
+            {
+                connection = value;
+                ConnectionChanged?.Invoke();
+            }
+        }
+    }
+
     internal InputProperty(Node node, string internalName, string displayName, object defaultValue, Type valueType)
     {
         InternalPropertyName = internalName;
@@ -133,7 +164,6 @@ public class InputProperty : IInputProperty
     }
 }
 
-
 public class InputProperty<T> : InputProperty, IInputProperty<T>
 {
     public new T Value
@@ -150,9 +180,9 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             {
                 return (T)FuncFactoryDelegate(func);
             }
-            
+
             object target = value;
-            if(value is ShaderExpressionVariable shaderExpression)
+            if (value is ShaderExpressionVariable shaderExpression)
             {
                 target = shaderExpression.GetConstant();
             }

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

@@ -1,9 +1,10 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IChunkRenderable
 {
-    public void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime);
+    public void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processingColorSpace);
 }

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

@@ -1,12 +1,13 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IPreviewRenderable
 {
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = ""); 
-    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName);
 }

+ 14 - 9
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -16,28 +16,33 @@ public interface IReadOnlyNode
     string DisplayName { get; }
 
     public void Execute(RenderContext context);
-    
-    /// <summary>
-    ///     Checks if the inputs are legal. If they are not, the node should not be executed.
-    /// Note that all nodes connected to any output of this node won't be executed either.
-    /// </summary>
-    /// <example>Divide node has two inputs, if the second input is 0, the node should not be executed. Since division by 0 is illegal</example>
-    /// <returns>True if the inputs are legal, false otherwise.</returns>
-    
+
     /// <summary>
     ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
     /// </summary>
     /// <param name="action">The action to perform on each node.</param>
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action);
 
+    /// <summary>
+    ///     Traverses the graph backwards from this node. Backwards means towards the input nodes.
+    /// </summary>
+    /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action);
+
     /// <summary>
     ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.
     /// </summary>
     /// <param name="action">The action to perform on each node.</param>
     public void TraverseForwards(Func<IReadOnlyNode, bool> action);
     
+     /// <summary>
+    ///     Traverses the graph forwards from this node. Forwards means towards the output nodes.
+    /// </summary>
+    /// <param name="action">The action to perform on each node. Input property is the input that was used to traverse this node.</param>
+    public void TraverseForwards(Func<IReadOnlyNode, IInputProperty, bool> action);
+
     public IInputProperty? GetInputProperty(string internalName);
     public IOutputProperty? GetOutputProperty(string internalName);
-    public void SerializeAdditionalData(Dictionary<string,object> additionalData);
+    public void SerializeAdditionalData(Dictionary<string, object> additionalData);
     public string GetNodeTypeUniqueName();
 }

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

@@ -8,6 +8,7 @@ public interface IReadOnlyShapeVectorData
 {
     public Matrix3X3 TransformationMatrix { get; }
     public Color StrokeColor { get; }
+    public bool Fill { get; }
     public Color FillColor { get; }
     public float StrokeWidth { get; }
     public RectD GeometryAABB { get; }

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

@@ -1,6 +1,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -12,7 +13,7 @@ public class SceneObjectRenderContext : RenderContext
     public RenderOutputProperty TargetPropertyOutput { get; }
 
     public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
-        ChunkResolution chunkResolution, VecI docSize, bool renderSurfaceIsScene, double opacity) : base(surface, frameTime, chunkResolution, docSize, opacity)
+        ChunkResolution chunkResolution, VecI docSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, double opacity) : base(surface, frameTime, chunkResolution, docSize, processingColorSpace, opacity)
     {
         TargetPropertyOutput = targetPropertyOutput;
         LocalBounds = localBounds;

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

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

+ 41 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -1,17 +1,18 @@
-using System.Collections;
-using System.Diagnostics;
+using System.Collections.Immutable;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 {
+    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
+    
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
-    public OutputNode? OutputNode => Nodes.OfType<OutputNode>().FirstOrDefault();
+    public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
+    public Node? CustomOutputNode { get; set; }
 
     IReadOnlyCollection<IReadOnlyNode> IReadOnlyNodeGraph.AllNodes => Nodes;
     IReadOnlyNode IReadOnlyNodeGraph.OutputNode => OutputNode;
@@ -22,8 +23,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
             return;
         }
-
+        
+        node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
+        ResetCache();
     }
 
     public void RemoveNode(Node node)
@@ -33,12 +36,19 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             return;
         }
 
+        node.ConnectionsChanged -= ResetCache;
         _nodes.Remove(node);
+        ResetCache();
     }
 
     public Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode outputNode)
     {
-        return GraphUtils.CalculateExecutionQueue(outputNode);
+        return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
+    }
+    
+    private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
+    {
+        return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
     }
 
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -57,11 +67,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
         if(OutputNode == null) return false;
         
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
             action(node);
         }
         
@@ -71,15 +80,16 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     public void Execute(RenderContext context)
     {
         if (OutputNode == null) return;
+        if(!CanExecute()) return;
 
-        var queue = CalculateExecutionQueue(OutputNode);
+        var queue = CalculateExecutionQueueInternal(OutputNode);
         
-        while (queue.Count > 0)
+        foreach (var node in queue)
         {
-            var node = queue.Dequeue();
-            
             if (node is Node typedNode)
             {
+                if(typedNode.IsDisposed) continue;
+                
                 typedNode.ExecuteInternal(context);
             }
             else
@@ -88,4 +98,22 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             }
         }
     }
+    
+    private bool CanExecute()
+    {
+        foreach (var node in Nodes)
+        {
+            if (node.IsDisposed)
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+    
+    private void ResetCache()
+    {
+        cachedExecutionList = null;
+    }
 }

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

@@ -119,15 +119,13 @@ public class CombineChannelsNode : RenderNode
         return finalBounds;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (Red.Value == null && Green.Value == null && Blue.Value == null && Alpha.Value == null)
         {
             return false;
         }
 
-        RenderContext context = new(renderOn, frame, resolution, VecI.One);
-        
         OnPaint(context, renderOn); 
         
         return true;

+ 2 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -97,18 +97,16 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
         return bounds;
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (Image.Value == null)
             return false;
 
-        RectD? bounds = GetPreviewBounds(frame, elementToRenderName);
+        RectD? bounds = GetPreviewBounds(context.FrameTime.Frame, elementToRenderName);
         
         if (bounds == null)
             return false;
         
-        RenderContext context = new(renderOn, frame, resolution, VecI.One);
-
         renderOn.Canvas.Save();
 
         _paint.ColorFilter = Grayscale.Value ? _redGrayscaleFilter : _redFilter;

+ 59 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -3,12 +3,14 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("CreateImage")]
-public class CreateImageNode : Node
+public class CreateImageNode : Node, IPreviewRenderable
 {
     public OutputProperty<Texture> Output { get; }
 
@@ -20,6 +22,8 @@ public class CreateImageNode : Node
 
     public RenderOutputProperty RenderOutput { get; }
 
+    private TextureCache textureCache = new();
+
     public CreateImageNode()
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -36,27 +40,76 @@ public class CreateImageNode : Node
             return;
         }
 
-        var surface = RequestTexture(0, Size.Value, false);
+        var surface = Render(context);
+
+        Output.Value = surface;
+
+        RenderOutput.ChainToPainterValue();
+    }
+
+    private Texture Render(RenderContext context)
+    {
+        var surface = textureCache.RequestTexture(0, Size.Value, context.ProcessingColorSpace, false);
 
         surface.DrawingSurface.Canvas.Clear(Fill.Value);
 
         int saved = surface.DrawingSurface.Canvas.Save();
 
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
-            context.DocumentSize);
+            context.DocumentSize, context.ProcessingColorSpace);
 
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
-        Output.Value = surface;
-
-        RenderOutput.ChainToPainterValue();
+        return surface;
     }
 
     private void OnPaint(RenderContext context, DrawingSurface surface)
     {
+        if(Output.Value == null || Output.Value.IsDisposed) return;
+        
         surface.Canvas.DrawSurface(Output.Value.DrawingSurface, 0, 0);
     }
 
     public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
+    }
+
+    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        {
+            return null;
+        }
+
+        return new RectD(0, 0, Size.Value.X, Size.Value.Y);
+    }
+
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        {
+            return false;
+        }
+
+        if (Output.Value == null)
+        {
+            return false;
+        }
+
+        var surface = Render(context);
+        
+        if (surface == null || surface.IsDisposed)
+        {
+            return false;
+        }
+        
+        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+        
+        return true;
+    }
 }

+ 70 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CustomOutputNode.cs

@@ -0,0 +1,70 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("CustomOutput")]
+public class CustomOutputNode : Node, IRenderInput, IPreviewRenderable
+{
+    public const string OutputNamePropertyName = "OutputName";
+    public RenderInputProperty Input { get; } 
+    public InputProperty<string> OutputName { get; }
+    
+    private VecI? lastDocumentSize;
+    public CustomOutputNode()
+    {
+        Input = new RenderInputProperty(this, OutputNode.InputPropertyName, "BACKGROUND", null);
+        AddInputProperty(Input);
+        
+        OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
+    }
+
+    public override Node CreateCopy()
+    {
+        return new CustomOutputNode();
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (context.TargetOutput == OutputName.Value)
+        {
+            lastDocumentSize = context.DocumentSize;
+
+            int saved = context.RenderSurface.Canvas.Save();
+            context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
+            Input.Value?.Paint(context, context.RenderSurface);
+
+            context.RenderSurface.Canvas.RestoreToCount(saved);
+        }
+    }
+
+    RenderInputProperty IRenderInput.Background => Input;
+    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (lastDocumentSize == null)
+        {
+            return null;
+        }
+        
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
+    }
+
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (Input.Value == null)
+        {
+            return false;
+        }
+        
+        int saved = renderOn.Canvas.Save();
+        Input.Value.Paint(context, renderOn);
+        
+        renderOn.Canvas.RestoreToCount(saved);
+        
+        return true;
+    }
+}

+ 0 - 57
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DebugBlendModeNode.cs

@@ -1,57 +0,0 @@
-using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
-using DrawingApiBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-// TODO: Add based on debug mode, not debug build.
-[NodeInfo("DebugBlendMode")]
-public class DebugBlendModeNode : Node
-{
-    private Paint _paint = new();
-    
-    public InputProperty<Texture?> Dst { get; }
-
-    public InputProperty<Texture?> Src { get; }
-
-    public InputProperty<DrawingApiBlendMode> BlendMode { get; }
-
-    public OutputProperty<Texture> Result { get; }
-
-    private Paint blendModeOpacityPaint => new() { BlendMode = DrawingApiBlendMode.SrcOver }; 
-    public DebugBlendModeNode()
-    {
-        Dst = CreateInput<Texture?>(nameof(Dst), "Dst", null);
-        Src = CreateInput<Texture?>(nameof(Src), "Src", null);
-        BlendMode = CreateInput(nameof(BlendMode), "Blend Mode", DrawingApiBlendMode.SrcOver);
-
-        Result = CreateOutput<Texture>(nameof(Result), "Result", null);
-    }
-
-    protected override void OnExecute(RenderContext context)
-    {
-        if (Dst.Value is not { } dst || Src.Value is not { } src)
-            return;
-
-        var size = new VecI(Math.Max(src.Size.X, dst.Size.X), int.Max(src.Size.Y, dst.Size.Y));
-        var workingSurface = RequestTexture(0, size);
-
-        workingSurface.DrawingSurface.Canvas.DrawSurface(dst.DrawingSurface, 0, 0, blendModeOpacityPaint);
-
-        _paint.BlendMode = BlendMode.Value;
-        workingSurface.DrawingSurface.Canvas.DrawSurface(src.DrawingSurface, 0, 0, _paint);
-        
-        Result.Value = workingSurface;
-    }
-
-
-    public override Node CreateCopy() => new DebugBlendModeNode();
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        _paint.Dispose();
-    }
-}

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

@@ -40,14 +40,12 @@ public class ApplyFilterNode : RenderNode, IRenderInput
         return PreviewUtils.FindPreviewBounds(Background.Connection, frame, elementToRenderName);
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (Background.Value == null)
             return false;
 
-        RenderContext context = new(renderOn, frame, ChunkResolution.Full, VecI.One);
-
         int layer = renderOn.Canvas.SaveLayer(_paint);
         Background.Value.Paint(context, renderOn);
         renderOn.Canvas.RestoreToCount(layer);

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

@@ -22,13 +22,18 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         AllowHighDpiRendering = true;
     }
 
-    public override Node CreateCopy() => new FolderNode { MemberName = MemberName };
+    public override Node CreateCopy() => new FolderNode
+    {
+        MemberName = MemberName, 
+        ClipToPreviousMember = this.ClipToPreviousMember,
+        EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
+    };
 
     public override VecD GetScenePosition(KeyFrameTime time) =>
-        documentSize / 2f; //GetTightBounds(time).GetValueOrDefault().Center;
+        documentSize / 2f; 
 
     public override VecD GetSceneSize(KeyFrameTime time) =>
-        documentSize; //GetTightBounds(time).GetValueOrDefault().Size;
+        documentSize; 
 
     protected override void OnExecute(RenderContext context)
     {
@@ -83,7 +88,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
     private void RenderFolderContent(SceneObjectRenderContext sceneContext, RectD bounds, bool useFilters)
     {
         VecI size = (VecI)bounds.Size;
-        var outputWorkingSurface = RequestTexture(0, size, true);
+        var outputWorkingSurface = RequestTexture(0, size, sceneContext.ProcessingColorSpace, true);
 
         blendPaint.ImageFilter = null;
         blendPaint.ColorFilter = null;
@@ -94,7 +99,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
-            Texture tempSurface = RequestTexture(1, outputWorkingSurface.Size);
+            Texture tempSurface = RequestTexture(1, outputWorkingSurface.Size, sceneContext.ProcessingColorSpace);
             if (Background.Connection.Node is IClipSource clipSource && ClipToPreviousMember)
             {
                 DrawClipSource(tempSurface.DrawingSurface, clipSource, sceneContext);
@@ -126,24 +131,31 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        RectI bounds = new RectI();
+        RectI? bounds = null;
         if (Content.Connection != null)
         {
             Content.Connection.Node.TraverseBackwards((n) =>
             {
-                if (n is ImageLayerNode imageLayerNode)
+                if (n is StructureNode structureNode)
                 {
-                    RectI? imageBounds = (RectI?)imageLayerNode.GetTightBounds(frameTime);
+                    RectI? imageBounds = (RectI?)structureNode.GetTightBounds(frameTime);
                     if (imageBounds != null)
                     {
-                        bounds = bounds.Union(imageBounds.Value);
+                        if (bounds == null)
+                        {
+                            bounds = imageBounds;
+                        }
+                        else
+                        {
+                            bounds = bounds.Value.Union(imageBounds.Value);
+                        }
                     }
                 }
 
                 return true;
             });
 
-            return (RectD)bounds;
+            return (RectD?)bounds ?? RectD.Empty;
         }
 
         return null;
@@ -165,32 +177,6 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         return guids;
     }
 
-    /// <summary>
-    /// Creates a clone of the folder, its mask and all of its children
-    /// </summary>
-    /*internal override Folder Clone()
-    {
-        var builder = ImmutableList<StructureMember>.Empty.ToBuilder();
-        for (var i = 0; i < Children.Count; i++)
-        {
-            var child = Children[i];
-            builder.Add(child.Clone());
-        }
-
-        return new Folder
-        {
-            GuidValue = GuidValue,
-            IsVisible = IsVisible,
-            Name = Name,
-            Opacity = Opacity,
-            Children = builder.ToImmutable(),
-            Mask = Mask?.CloneFromCommitted(),
-            BlendMode = BlendMode,
-            ClipToMemberBelow = ClipToMemberBelow,
-            MaskIsVisible = MaskIsVisible
-        };
-    }*/
-
     public override RectD? GetPreviewBounds(int frame, string elementFor = "")
     {
         if (elementFor == nameof(EmbeddedMask))
@@ -201,16 +187,14 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
         return GetTightBounds(frame);
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.RenderPreview(renderOn, resolution, frame, elementToRenderName);
+            return base.RenderPreview(renderOn, context, elementToRenderName);
         }
 
-        // TODO: Make preview better, with filters, clips and stuff
-
         if (Content.Connection != null)
         {
             var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node);
@@ -219,7 +203,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
                 IReadOnlyNode node = executionQueue.Dequeue();
                 if (node is IPreviewRenderable previewRenderable)
                 {
-                    previewRenderable.RenderPreview(renderOn, resolution, frame, elementToRenderName);
+                    previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
                 }
             }
         }

+ 30 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -6,6 +6,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -23,19 +24,21 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public bool LockTransparency { get; set; }
 
     private VecI startSize;
+    private ColorSpace colorSpace;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
     private Texture fullResrenderedSurface;
     private int renderedSurfaceFrame = -1;
 
-    public ImageLayerNode(VecI size)
+    public ImageLayerNode(VecI size, ColorSpace colorSpace)
     {
         if (keyFrames.Count == 0)
         {
-            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size) });
+            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size, colorSpace) });
         }
 
         this.startSize = size;
+        this.colorSpace = colorSpace;
     }
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
@@ -136,7 +139,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
     }
 
-    public override bool RenderPreview(DrawingSurface renderOnto, ChunkResolution resolution, int frame,
+    public override bool RenderPreview(DrawingSurface renderOnto, RenderContext context,
         string elementToRenderName)
     {
         if (IsDisposed)
@@ -146,11 +149,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
         if (elementToRenderName == nameof(EmbeddedMask))
         {
-            return base.RenderPreview(renderOnto, resolution, frame, elementToRenderName);
+            return base.RenderPreview(renderOnto, context, elementToRenderName);
         }
 
-        var img = GetLayerImageAtFrame(frame);
+        var img = GetLayerImageAtFrame(context.FrameTime.Frame);
 
+        int cacheFrame = context.FrameTime.Frame;
         if (Guid.TryParse(elementToRenderName, out Guid guid))
         {
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
@@ -158,6 +162,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             if (keyFrame != null)
             {
                 img = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
+                cacheFrame = keyFrame.StartFrame;
+            }
+            else if (guid == Id)
+            {
+                img = GetLayerImageAtFrame(0);
+                cacheFrame = 0;
             }
         }
 
@@ -166,7 +176,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             return false;
         }
 
-        if (renderedSurfaceFrame == frame)
+        if (renderedSurfaceFrame == cacheFrame)
         {
             renderOnto.Canvas.DrawSurface(fullResrenderedSurface.DrawingSurface, VecI.Zero, blendPaint);
         }
@@ -174,7 +184,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         {
             img.DrawMostUpToDateRegionOn(
                 new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
-                resolution,
+                context.ChunkResolution,
                 renderOnto, VecI.Zero, blendPaint);
         }
 
@@ -183,6 +193,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     {
+        if (keyFrames.Count == 1)
+        {
+            return keyFrames[0];
+        }
+        
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         if (imageFrame?.Data is not ChunkyImage)
         {
@@ -212,7 +227,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override Node CreateCopy()
     {
-        var image = new ImageLayerNode(startSize) { MemberName = this.MemberName, };
+        var image = new ImageLayerNode(startSize, colorSpace)
+        {
+            MemberName = this.MemberName, LockTransparency = this.LockTransparency,
+            ClipToPreviousMember = this.ClipToPreviousMember, EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
+        };
 
         image.keyFrames.Clear();
 
@@ -239,13 +258,13 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
 
-    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime)
+    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processColorSpace)
     {
-        base.RenderChunk(chunkPos, resolution, frameTime);
+        base.RenderChunk(chunkPos, resolution, frameTime, processColorSpace);
 
         var img = GetLayerImageAtFrame(frameTime.Frame);
 
-        RenderChunkyImageChunk(chunkPos, resolution, img, 85, ref fullResrenderedSurface);
+        RenderChunkyImageChunk(chunkPos, resolution, img, 85, processColorSpace, ref fullResrenderedSurface);
         renderedSurfaceFrame = frameTime.Frame;
     }
 

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

@@ -56,11 +56,6 @@ public class MergeNode : RenderNode
             int saved = target.Canvas.SaveLayer();
             Bottom.Value.Paint(context, target);
 
-            if (paint == null)
-            {
-                paint = new Paint();
-            }
-            
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             
@@ -103,14 +98,13 @@ public class MergeNode : RenderNode
         return totalBounds;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (Top.Value == null && Bottom.Value == null)
         {
             return false;
         }
 
-        RenderContext context = new RenderContext(renderOn, frame, ChunkResolution.Full, VecI.Zero);
         Merge(renderOn, context);
 
         return true;

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -3,6 +3,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
@@ -18,6 +19,8 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
     public FuncOutputProperty<Float2> Coordinate { get; }
 
     public FuncOutputProperty<Half4> Color { get; }
+    
+    public InputProperty<ColorSampleMode> SampleMode { get; }
 
     public Guid OtherNode { get; set; }
     
@@ -26,6 +29,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         Image = CreateInput<Texture?>("Surface", "IMAGE", null);
         Coordinate = CreateFuncOutput("Coordinate", "UV", ctx => ctx.OriginalPosition);
         Color = CreateFuncOutput("Color", "COLOR", GetColor);
+        SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
     }
     
     private Half4 GetColor(FuncContext context)
@@ -37,7 +41,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
             return new Half4("") { ConstantValue = Colors.Transparent };
         }
 
-        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition);
+        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value);
     }
 
     protected override void OnExecute(RenderContext context)
@@ -55,7 +59,7 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         return new RectD(0, 0, Image.Value.Size.X, Image.Value.Size.Y);
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if(Image.Value is null)
         {

+ 29 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -23,7 +23,11 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
 
     private string _lastSksl;
+    private VecI? size;
 
+    // TODO: Add caching
+    // Caching requires a way to check if any connected node changed, checking inputs for this node works
+    // Also gather uniforms without doing full string builder generation of the shader
 
     public ModifyImageRightNode()
     {
@@ -33,10 +37,10 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
 
     protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
     {
-        if (OtherNode == null)
+        if (OtherNode == null || OtherNode == default)
         {
-            FindStartNode();
-            if (OtherNode == null)
+            OtherNode = FindStartNode()?.Id ?? default;
+            if (OtherNode == null || OtherNode == default)
             {
                 return;
             }
@@ -48,12 +52,16 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             return;
         }
 
-        if (startNode.Image.Value is not { Size: var size })
+        OtherNode = startNode.Id;
+
+        if (startNode.Image.Value is not { Size: var imgSize })
         {
             return;
         }
 
-        ShaderBuilder builder = new(size);
+        size = imgSize;
+
+        ShaderBuilder builder = new(size.Value);
         FuncContext context = new(renderContext, builder);
 
         if (Coordinate.Connection != null)
@@ -100,19 +108,31 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, size.X, size.Y, drawingPaint);
+        targetSurface.Canvas.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
         builder.Dispose();
     }
 
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
-        //TODO: Implement
+        var startNode = FindStartNode();
+        if (startNode != null)
+        {
+            return startNode.GetPreviewBounds(frame, elementToRenderName);
+        }
+
         return null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
-        //TODO: Implement
+        var startNode = FindStartNode();
+        if (drawingPaint != null && startNode is { Image.Value: not null })
+        {
+            renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+
+            return true;
+        }
+
         return false;
     }
 
@@ -130,7 +150,6 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             if (node is ModifyImageLeftNode leftNode)
             {
                 startNode = leftNode;
-                OtherNode = leftNode.Id;
                 return false;
             }
 

+ 68 - 27
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -10,6 +10,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Shaders.Generation;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -26,7 +27,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyList<InputProperty> InputProperties => inputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
-
+    public event Action ConnectionsChanged;
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
@@ -41,11 +42,9 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
     protected virtual bool ExecuteOnlyOnCacheChange => false;
 
-    protected bool IsDisposed => _isDisposed;
+    protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
-
-    private Dictionary<int, Texture> _managedTextures = new();
-
+    
     public void Execute(RenderContext context)
     {
         ExecuteInternal(context);
@@ -83,28 +82,34 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
-    protected Texture RequestTexture(int id, VecI size, bool clear = true)
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action)
     {
-        if (_managedTextures.TryGetValue(id, out var texture))
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
         {
-            if (texture.Size != size || texture.IsDisposed)
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
             {
-                texture.Dispose();
-                texture = new Texture(size);
-                _managedTextures[id] = texture;
-                return texture;
+                continue;
             }
 
-            if (clear)
+            if (!action(node.Item1, node.Item2))
             {
-                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
+                return;
             }
 
-            return texture;
+            foreach (var inputProperty in node.Item1.InputProperties)
+            {
+                if (inputProperty.Connection != null)
+                {
+                    queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
+                }
+            }
         }
-
-        _managedTextures[id] = new Texture(size);
-        return _managedTextures[id];
     }
 
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
@@ -170,6 +175,39 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+    public void TraverseForwards(Func<IReadOnlyNode, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2))
+            {
+                return;
+            }
+
+            foreach (var outputProperty in node.Item1.OutputProperties)
+            {
+                foreach (var connection in outputProperty.Connections)
+                {
+                    if (connection.Connection != null)
+                    {
+                        queueNodes.Enqueue((connection.Node, connection));
+                    }
+                }
+            }
+        }
+    }
+
     public void RemoveKeyFrame(Guid keyFrameId)
     {
         keyFrames.RemoveAll(x => x.KeyFrameGuid == keyFrameId);
@@ -232,6 +270,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -244,6 +283,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {propName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
         return property;
     }
@@ -275,6 +315,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
             throw new InvalidOperationException($"Input with name {property.InternalPropertyName} already exists.");
         }
 
+        property.ConnectionChanged += InvokeConnectionsChanged;
         inputs.Add(property);
     }
 
@@ -307,11 +348,6 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 keyFrame.Dispose();
             }
         }
-
-        foreach (var texture in _managedTextures)
-        {
-            texture.Value.Dispose();
-        }
     }
 
     public void DisconnectAll()
@@ -359,10 +395,10 @@ public abstract class Node : IReadOnlyNode, IDisposable
             object value = CloneValue(toClone.NonOverridenValue, clone.inputs[i]);
             clone.inputs[i].NonOverridenValue = value;
         }
-        
+
         // This makes shader outputs copy old delegate, also I don't think it's required because output is calculated based on inputs,
         // leaving commented in case I'm wrong
-        
+
         /*for (var i = 0; i < clone.outputs.Count; i++)
         {
             var cloneOutput = outputs[i];
@@ -426,6 +462,11 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return new None();
     }
     
+    private void InvokeConnectionsChanged()
+    {
+        ConnectionsChanged?.Invoke();
+    }
+
     private static object CloneValue(object? value, InputProperty? input)
     {
         if (value is null)
@@ -441,7 +482,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 return input.FuncFactory(expr.GetConstant());
             }
         }
-        
+
         if (value is ICloneable cloneable)
         {
             return cloneable.Clone();
@@ -452,7 +493,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         {
             return value;
         }
-        
+
         return default;
     }
 }

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

@@ -94,7 +94,7 @@ public class NoiseNode : RenderNode
         return new RectD(0, 0, 128, 128); 
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var shader = SelectShader();
         if (shader == null)

+ 15 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs

@@ -10,11 +10,13 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("Output")]
 public class OutputNode : Node, IRenderInput, IPreviewRenderable
 {
+    public const string UniqueName = "PixiEditor.Output";
     public const string InputPropertyName = "Background";
 
-    public RenderInputProperty Input { get; } 
-    
+    public RenderInputProperty Input { get; }
+
     private VecI? lastDocumentSize;
+
     public OutputNode()
     {
         Input = new RenderInputProperty(this, InputPropertyName, "BACKGROUND", null);
@@ -28,39 +30,41 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
     protected override void OnExecute(RenderContext context)
     {
+        if (!string.IsNullOrEmpty(context.TargetOutput)) return;
+
         lastDocumentSize = context.DocumentSize;
-        
+
         int saved = context.RenderSurface.Canvas.Save();
         context.RenderSurface.Canvas.ClipRect(new RectD(0, 0, context.DocumentSize.X, context.DocumentSize.Y));
         Input.Value?.Paint(context, context.RenderSurface);
-        
+
         context.RenderSurface.Canvas.RestoreToCount(saved);
     }
 
     RenderInputProperty IRenderInput.Background => Input;
+
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         if (lastDocumentSize == null)
         {
             return null;
         }
-        
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y); 
+
+        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         if (Input.Value == null)
         {
             return false;
         }
-        
-        RenderContext context = new(renderOn, frame, resolution, VecI.One);
+
         int saved = renderOn.Canvas.Save();
         Input.Value.Paint(context, renderOn);
-        
+
         renderOn.Canvas.RestoreToCount(saved);
-        
+
         return true;
     }
 }

+ 24 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -1,8 +1,9 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -13,6 +14,8 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public bool AllowHighDpiRendering { get; set; } = false;
 
+    private TextureCache textureCache = new();
+
     public RenderNode()
     {
         Painter painter = new Painter(Paint);
@@ -31,22 +34,22 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
             }
         }
     }
-    
+
     private void Paint(RenderContext context, DrawingSurface surface)
     {
         DrawingSurface target = surface;
-        bool useIntermediate = !AllowHighDpiRendering 
-                               && context.DocumentSize is { X: > 0, Y: > 0 } 
+        bool useIntermediate = !AllowHighDpiRendering
+                               && context.DocumentSize is { X: > 0, Y: > 0 }
                                && surface.DeviceClipBounds.Size != context.DocumentSize;
         if (useIntermediate)
         {
-            Texture intermediate = RequestTexture(0, context.DocumentSize);
+            Texture intermediate = textureCache.RequestTexture(0, context.DocumentSize, context.ProcessingColorSpace);
             target = intermediate.DrawingSurface;
         }
 
         OnPaint(context, target);
-        
-        if(useIntermediate)
+
+        if (useIntermediate)
         {
             surface.Canvas.DrawSurface(target, 0, 0);
         }
@@ -56,6 +59,19 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
     public abstract RectD? GetPreviewBounds(int frame, string elementToRenderName = "");
 
-    public abstract bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public abstract bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName);
+
+    protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
+    {
+        return textureCache.RequestTexture(id, size, processingCs, clear);
+    }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose(); 
+    }
+
+   
 }

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Numerics;
@@ -17,11 +18,14 @@ public class SampleImageNode : Node
 
     public FuncOutputProperty<Half4> Color { get; }
 
+    public InputProperty<ColorSampleMode> SampleMode { get; }
+
     public SampleImageNode()
     {
         Image = CreateInput<Texture>(nameof(Texture), "IMAGE", null);
         Coordinate = CreateFuncInput<Float2>(nameof(Coordinate), "UV", VecD.Zero);
         Color = CreateFuncOutput(nameof(Color), "COLOR", GetColor);
+        SampleMode = CreateInput(nameof(SampleMode), "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
     }
 
     private Half4 GetColor(FuncContext context)
@@ -35,12 +39,12 @@ public class SampleImageNode : Node
 
         Expression uv = context.GetValue(Coordinate);
 
-        return context.SampleSurface(Image.Value.DrawingSurface, uv);
+        return context.SampleSurface(Image.Value.DrawingSurface, uv, SampleMode.Value);
     }
 
     protected override void OnExecute(RenderContext context)
     {
-        
+
     }
 
     public override Node CreateCopy() => new SampleImageNode();

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

@@ -3,6 +3,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -29,42 +30,46 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         Radius = radius;
     }
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         int saved = 0;
         if (applyTransform)
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
-        using Paint shapePaint = new Paint() { IsAntiAliased = true };
+        using Paint shapePaint = new Paint();
+        shapePaint.IsAntiAliased = true;
 
-        shapePaint.Color = FillColor;
-        shapePaint.Style = PaintStyle.Fill;
-        drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+        if (Fill)
+        {
+            shapePaint.Color = FillColor;
+            shapePaint.Style = PaintStyle.Fill;
+            canvas.DrawOval(Center, Radius, shapePaint);
+        }
 
         if (StrokeWidth > 0)
         {
             shapePaint.Color = StrokeColor;
             shapePaint.Style = PaintStyle.Stroke;
             shapePaint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+            canvas.DrawOval(Center, Radius, shapePaint);
         }
 
         if (applyTransform)
         {
-            drawingSurface.Canvas.RestoreToCount(saved);
+            canvas.RestoreToCount(saved);
         }
     }
 
@@ -83,14 +88,16 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         return CalculateHash();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new EllipseVectorData(Center, Radius)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
+       
+    }
+
+    public override VectorPath ToPath()
+    {
+        // TODO: Apply transformation matrix
+        VectorPath path = new VectorPath();
+        path.AddOval(RectD.FromCenterAndSize(Center, Radius * 2));
+        return path;
     }
 }

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

@@ -4,14 +4,15 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnlyLineData
+public class LineVectorData : ShapeVectorData, IReadOnlyLineData
 {
-    public VecD Start { get; set; } = startPos; // Relative to the document top left
-    public VecD End { get; set; } = pos; // Relative to the document top left
+    public VecD Start { get; set; }
+    public VecD End { get; set; }
 
     public VecD TransformedStart
     {
@@ -52,23 +53,32 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
     public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
         .WithMatrix(TransformationMatrix);
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+
+    public LineVectorData(VecD startPos, VecD pos)
+    {
+        Start = startPos;
+        End = pos;
+        
+        Fill = false;
+    }
+
+    public override void RasterizeGeometry(Canvas canvas)
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         int num = 0;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         using Paint paint = new Paint() { IsAntiAliased = true };
@@ -77,11 +87,11 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
         paint.Style = PaintStyle.Stroke;
         paint.StrokeWidth = StrokeWidth;
 
-        drawingSurface.Canvas.DrawLine(Start, End, paint);
+        canvas.DrawLine(Start, End, paint);
 
         if (applyTransform)
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
     }
 
@@ -100,11 +110,13 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
         return GetCacheHash();
     }
 
-    public override object Clone()
+    public override VectorPath ToPath()
     {
-        return new LineVectorData(Start, End)
-        {
-            StrokeColor = StrokeColor, StrokeWidth = StrokeWidth, TransformationMatrix = TransformationMatrix
-        };
+        // TODO: Apply transformation matrix
+
+        VectorPath path = new VectorPath();
+        path.MoveTo((VecF)Start);
+        path.LineTo((VecF)End);
+        return path;
     }
 }

+ 33 - 24
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -10,59 +10,66 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
-    public VectorPath Path { get; }
+    public VectorPath Path { get; set; }
     public override RectD GeometryAABB => Path.TightBounds;
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
+    public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
+    
+    public StrokeJoin StrokeLineJoin { get; set; } = StrokeJoin.Round;
+
     public PathVectorData(VectorPath path)
     {
         Path = path;
     }
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas canvas)
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         int num = 0;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
         using Paint paint = new Paint()
         {
-            IsAntiAliased = true, StrokeJoin = StrokeJoin.Round, StrokeCap = StrokeCap.Round
+            IsAntiAliased = true, StrokeJoin = StrokeLineJoin, StrokeCap = StrokeLineCap
         };
 
-        if (FillColor.A > 0)
+        if (Fill && FillColor.A > 0)
         {
             paint.Color = FillColor;
             paint.Style = PaintStyle.Fill;
 
-            drawingSurface.Canvas.DrawPath(Path, paint);
+            canvas.DrawPath(Path, paint);
         }
 
-        paint.Color = StrokeColor;
-        paint.Style = PaintStyle.Stroke;
-        paint.StrokeWidth = StrokeWidth;
-
-        drawingSurface.Canvas.DrawPath(Path, paint);
+        if (StrokeWidth > 0)
+        {
+            paint.Color = StrokeColor;
+            paint.Style = PaintStyle.Stroke;
+            paint.StrokeWidth = StrokeWidth;
+            
+            canvas.DrawPath(Path, paint);
+        }
 
         if (applyTransform)
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
     }
 
@@ -81,14 +88,16 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
         return Path.GetHashCode();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new PathVectorData(new VectorPath(Path))
+        if (copy is PathVectorData pathData)
         {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
+            pathData.Path = new VectorPath(Path);
+        }
+    }
+
+    public override VectorPath ToPath()
+    {
+        return new VectorPath(Path);
     }
 }

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

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -22,17 +23,17 @@ public class PointsVectorData : ShapeVectorData
     public override ShapeCorners TransformationCorners => new ShapeCorners(
         GeometryAABB).WithMatrix(TransformationMatrix);
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas drawingSurface)
     {
         Rasterize(drawingSurface, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         using Paint paint = new Paint();
         paint.Color = FillColor;
@@ -41,17 +42,17 @@ public class PointsVectorData : ShapeVectorData
         int num = 0;
         if (applyTransform)
         {
-            num = drawingSurface.Canvas.Save();
+            num = canvas.Save();
             Matrix3X3 final = TransformationMatrix;
-            drawingSurface.Canvas.SetMatrix(final);
+            canvas.SetMatrix(final);
         }
 
-        drawingSurface.Canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((int)p.X, (int)p.Y)).ToArray(),
+        canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((int)p.X, (int)p.Y)).ToArray(),
             paint);
 
         if (applyTransform)
         {
-            drawingSurface.Canvas.RestoreToCount(num);
+            canvas.RestoreToCount(num);
         }
     }
 
@@ -70,11 +71,23 @@ public class PointsVectorData : ShapeVectorData
         return Points.GetHashCode();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new PointsVectorData(Points)
+        if (copy is PointsVectorData pointsVectorData)
         {
-            StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
-        };
+            pointsVectorData.Points = new List<VecD>(Points);
+        }
+    }
+
+    public override VectorPath ToPath()
+    {
+        VectorPath path = new VectorPath();
+        
+        foreach (VecD point in Points)
+        {
+            path.LineTo((VecF)point);
+        }
+        
+        return path;
     }
 }

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

@@ -3,6 +3,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -33,31 +34,41 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         Center = center;
         Size = size;
     }
+    
+    public RectangleVectorData(double x, double y, double width, double height)
+    {
+        Center = new VecD(x + width / 2, y + height / 2);
+        Size = new VecD(width, height);
+    }
 
-    public override void RasterizeGeometry(DrawingSurface drawingSurface)
+    public override void RasterizeGeometry(Canvas canvas)
     {
-        Rasterize(drawingSurface, false);
+        Rasterize(canvas, false);
     }
 
-    public override void RasterizeTransformed(DrawingSurface drawingSurface)
+    public override void RasterizeTransformed(Canvas canvas)
     {
-        Rasterize(drawingSurface, true);
+        Rasterize(canvas, true);
     }
 
-    private void Rasterize(DrawingSurface drawingSurface, bool applyTransform)
+    private void Rasterize(Canvas canvas, bool applyTransform)
     {
         int saved = 0;
         if (applyTransform)
         {
-            saved = drawingSurface.Canvas.Save();
-            ApplyTransformTo(drawingSurface);
+            saved = canvas.Save();
+            ApplyTransformTo(canvas);
         }
 
-        using Paint paint = new Paint() { IsAntiAliased = true };
+        using Paint paint = new Paint();
+        paint.IsAntiAliased = true;
 
-        paint.Color = FillColor;
-        paint.Style = PaintStyle.Fill;
-        drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        if (Fill && FillColor.A > 0)
+        {
+            paint.Color = FillColor;
+            paint.Style = PaintStyle.Fill;
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        }
 
         if (StrokeWidth > 0)
         {
@@ -65,12 +76,12 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             paint.Style = PaintStyle.Stroke;
 
             paint.StrokeWidth = StrokeWidth;
-            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
         }
 
         if (applyTransform)
         {
-            drawingSurface.Canvas.RestoreToCount(saved);
+            canvas.RestoreToCount(saved);
         }
     }
 
@@ -89,14 +100,10 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         return CalculateHash();
     }
 
-    public override object Clone()
+    public override VectorPath ToPath()
     {
-        return new RectangleVectorData(Center, Size)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
+        VectorPath path = new VectorPath();
+        path.AddRect(RectD.FromCenterAndSize(Center, Size));
+        return path;
     }
 }

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

@@ -4,6 +4,7 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
@@ -15,30 +16,41 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     public Color StrokeColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public float StrokeWidth { get; set; } = 1;
+    public bool Fill { get; set; } = true;
     public abstract RectD GeometryAABB { get; } 
     public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public abstract ShapeCorners TransformationCorners { get; } 
     
-    protected void ApplyTransformTo(DrawingSurface drawingSurface)
+    protected void ApplyTransformTo(Canvas canvas)
     {
-        Matrix3X3 canvasMatrix = drawingSurface.Canvas.TotalMatrix;
+        Matrix3X3 canvasMatrix = canvas.TotalMatrix;
 
         Matrix3X3 final = canvasMatrix.Concat(TransformationMatrix);
 
-        drawingSurface.Canvas.SetMatrix(final);
+        canvas.SetMatrix(final);
     }
 
-    public abstract void RasterizeGeometry(DrawingSurface drawingSurface);
-    public abstract void RasterizeTransformed(DrawingSurface drawingSurface);
+    public abstract void RasterizeGeometry(Canvas canvas);
+    public abstract void RasterizeTransformed(Canvas canvas);
     public abstract bool IsValid();
     public abstract int GetCacheHash();
     public abstract int CalculateHash();
-    public abstract object Clone();
+
+    public object Clone()
+    {
+        ShapeVectorData copy = (ShapeVectorData)MemberwiseClone();
+        AdjustCopy(copy);
+        return copy;
+    }
+
+    protected virtual void AdjustCopy(ShapeVectorData copy) { }
 
     public override int GetHashCode()
     {
         return CalculateHash();
     }
+
+    public abstract VectorPath ToPath();
 }

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

@@ -26,7 +26,7 @@ public class RasterizeShapeNode : RenderNode
         if (shape == null || !shape.IsValid())
             return;
         
-        shape.RasterizeTransformed(surface);
+        shape.RasterizeTransformed(surface.Canvas);
     }
 
     public override Node CreateCopy() => new RasterizeShapeNode();
@@ -35,14 +35,14 @@ public class RasterizeShapeNode : RenderNode
         return Data?.Value?.TransformedAABB;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame, string elementToRenderName)
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
         var shape = Data.Value;
 
         if (shape == null || !shape.IsValid())
             return false;
 
-        shape.RasterizeTransformed(renderOn);
+        shape.RasterizeTransformed(renderOn.Canvas);
 
         return true;
     }

+ 0 - 14
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs

@@ -23,21 +23,7 @@ public abstract class ShapeNode<T> : Node where T : ShapeVectorData
         var data = GetShapeData(context);
 
         Output.Value = data;
-        
-        /*if (data == null || !data.IsValid())
-            return;
-
-        return RasterizePreview(data, context.DocumentSize);*/
     }
     
     protected abstract T? GetShapeData(RenderContext context);
-
-    public Texture RasterizePreview(ShapeVectorData vectorData, VecI size)
-    {
-        Texture texture = RequestTexture(0, size);
-        
-        vectorData.RasterizeTransformed(texture.DrawingSurface);
-        
-        return texture;
-    }
 }

+ 7 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -7,6 +7,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
@@ -144,6 +145,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
             context.FrameTime, context.ChunkResolution, context.DocumentSize, renderTarget == context.RenderSurface,
+            context.ProcessingColorSpace,
             context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
         return renderObjectContext;
@@ -192,13 +194,13 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         maskCacheHash = EmbeddedMask?.GetCacheHash() ?? 0;
     }
 
-    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime)
+    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processingColorSpace)
     {
-        RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, ref renderedMask);
+        RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
     }
 
     protected void RenderChunkyImageChunk(VecI chunkPos, ChunkResolution resolution, ChunkyImage img,
-        int textureId,
+        int textureId, ColorSpace processingColorSpace,
         ref Texture? renderSurface)
     {
         if (img is null)
@@ -208,7 +210,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
         VecI targetSize = img.LatestSize;
         
-        renderSurface = RequestTexture(textureId, targetSize, false);
+        renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
 
         int saved = renderSurface.DrawingSurface.Canvas.Save();
         
@@ -300,7 +302,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         return null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (elementToRenderName != nameof(EmbeddedMask))

+ 37 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TextureCache.cs

@@ -1,29 +1,56 @@
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-public class TextureCache
+public class TextureCache : IDisposable
 {
-    private Dictionary<ChunkResolution, Texture> _cachedTextures = new();
-    
-    public Texture GetTexture(ChunkResolution resolution, VecI size)
+    private Dictionary<int, Texture> _managedTextures = new();
+
+    public Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     {
-        if (_cachedTextures.TryGetValue(resolution, out var texture) && texture.Size == size)
+        if (_managedTextures.TryGetValue(id, out var texture))
         {
+            if (texture.Size != size || texture.IsDisposed || texture.ColorSpace != processingCs)
+            {
+                texture.Dispose();
+                texture = new Texture(CreateImageInfo(size, processingCs));
+                _managedTextures[id] = texture;
+                return texture;
+            }
+
+            if (clear)
+            {
+                texture.DrawingSurface.Canvas.Clear(Colors.Transparent);
+            }
+
             return texture;
         }
 
-        texture = new Texture(size);
-        _cachedTextures[resolution] = texture;
-        return texture;
+        _managedTextures[id] = new Texture(CreateImageInfo(size, processingCs));
+        return _managedTextures[id];
+    }
+
+    private ImageInfo CreateImageInfo(VecI size, ColorSpace processingCs)
+    {
+        if (processingCs == null)
+        {
+            return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, ColorSpace.CreateSrgbLinear())
+            {
+                GpuBacked = true
+            };
+        }
+
+        return new ImageInfo(size.X, size.Y, ColorType.RgbaF16, AlphaType.Premul, processingCs) { GpuBacked = true };
     }
 
     public void Dispose()
     {
-        foreach (var texture in _cachedTextures.Values)
+        foreach (var texture in _managedTextures)
         {
-            texture.Dispose();
+            texture.Value.Dispose();
         }
     }
 }

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

@@ -85,7 +85,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         return null;
     }
 
-    public override bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
         if (ShapeData == null)
@@ -160,13 +160,13 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     public void Rasterize(DrawingSurface surface, Paint paint)
     {
         int layer = surface.Canvas.SaveLayer(paint);
-        ShapeData?.RasterizeTransformed(surface);
+        ShapeData?.RasterizeTransformed(surface.Canvas);
         
         surface.Canvas.RestoreToCount(layer);
     }
 
     public override Node CreateCopy()
     {
-        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), };
+        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), ClipToPreviousMember = this.ClipToPreviousMember };
     }
 }

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -102,4 +102,6 @@ public interface IReadOnlyDocument : IDisposable
     IReadOnlyList<IReadOnlyStructureNode> FindMemberPath(Guid guid);
     IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public DocumentRenderer Renderer { get; }
+    public ColorSpace ProcessingColorSpace { get; }
+    public void InitProcessingColorSpace(ColorSpace processingColorSpace);
 }

+ 21 - 3
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs → src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs

@@ -4,7 +4,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
 
-internal class CreateRasterKeyFrame_Change : Change
+internal class CreateCel_Change : Change
 {
     private readonly Guid _targetLayerGuid;
     private int _frame;
@@ -14,7 +14,7 @@ internal class CreateRasterKeyFrame_Change : Change
     private Guid createdKeyFrameId;
 
     [GenerateMakeChangeAction]
-    public CreateRasterKeyFrame_Change(Guid targetLayerGuid, Guid newKeyFrameGuid, int frame,
+    public CreateCel_Change(Guid targetLayerGuid, Guid newKeyFrameGuid, int frame,
         int cloneFromFrame = -1,
         Guid cloneFromExisting = default)
     {
@@ -27,6 +27,18 @@ internal class CreateRasterKeyFrame_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
+        var targetLayer = target.FindMember(_targetLayerGuid);
+        
+        if (targetLayer is null)
+        {
+            return false;
+        }
+        
+        if(_frame == -1 && targetLayer.KeyFrames.All(x => x.KeyFrameGuid != createdKeyFrameId))
+        {
+            return false;
+        }
+        
         return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);
     }
 
@@ -39,7 +51,7 @@ internal class CreateRasterKeyFrame_Change : Change
 
         ImageLayerNode targetNode = target.FindMemberOrThrow<ImageLayerNode>(_targetLayerGuid);
 
-        ChunkyImage img = cloneFromImage?.CloneFromCommitted() ?? new ChunkyImage(target.Size);
+        ChunkyImage img = cloneFromImage?.CloneFromCommitted() ?? new ChunkyImage(target.Size, target.ProcessingColorSpace);
 
         var keyFrame =
             new RasterKeyFrame(createdKeyFrameId, targetNode.Id, _frame, target);
@@ -51,6 +63,11 @@ internal class CreateRasterKeyFrame_Change : Change
 
         if (existingData is null)
         {
+            if (_frame == -1)
+            {
+                ignoreInUndo = true;
+                return new None();
+            }
             targetNode.AddFrame(createdKeyFrameId,
                 new KeyFrameData(createdKeyFrameId, _frame, 1, ImageLayerNode.ImageLayerKey) { Data = img, });
         }
@@ -81,6 +98,7 @@ internal class CreateRasterKeyFrame_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.AnimationData.RemoveKeyFrame(createdKeyFrameId);
+        target.FindMemberOrThrow<ImageLayerNode>(_targetLayerGuid).RemoveKeyFrame(createdKeyFrameId);
         return new DeleteKeyFrame_ChangeInfo(createdKeyFrameId);
     }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs

@@ -42,6 +42,7 @@ internal class DeleteKeyFrame_Change : Change
         out bool ignoreInUndo)
     {
         target.AnimationData.RemoveKeyFrame(_keyFrameId);
+        target.FindNode<Node>(clonedKeyFrame.NodeId).RemoveKeyFrame(_keyFrameId);
         ignoreInUndo = false;
         return new DeleteKeyFrame_ChangeInfo(_keyFrameId);
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFramesStartPos_UpdateableChange.cs

@@ -3,7 +3,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
 
-internal class KeyFramesStartPos_UpdateableChange : UpdateableChange
+internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChange
 {
     public Guid[] KeyFramesGuid { get;  }
     public int Delta { get; set; }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs

@@ -37,7 +37,7 @@ internal class ApplyLayerMask_Change : Change
             throw new InvalidOperationException("Cannot apply layer mask, no mask");
 
         var layerImage = layer.GetLayerImageAtFrame(frame);
-        ChunkyImage newLayerImage = new ChunkyImage(target.Size);
+        ChunkyImage newLayerImage = new ChunkyImage(target.Size, target.ProcessingColorSpace);
         newLayerImage.AddRasterClip(layer.EmbeddedMask);
         newLayerImage.EnqueueDrawCommitedChunkyImage(VecI.Zero, layerImage);
         newLayerImage.CommitChanges();
@@ -68,7 +68,7 @@ internal class ApplyLayerMask_Change : Change
         if (savedLayer is null || savedMask is null)
             throw new InvalidOperationException("Cannot restore layer mask, no saved data");
 
-        ChunkyImage newMask = new ChunkyImage(target.Size);
+        ChunkyImage newMask = new ChunkyImage(target.Size, target.ProcessingColorSpace);
         savedMask.ApplyChunksToImage(newMask);
         var affectedChunksMask = newMask.FindAffectedArea();
         newMask.CommitChanges();

+ 191 - 57
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -5,9 +5,12 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
@@ -19,7 +22,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
     private Guid targetLayerGuid;
     private Dictionary<int, CommittedChunkStorage> originalChunks = new();
-    
+
+    private Dictionary<int, VectorPath> originalPaths = new();
+
 
     [GenerateMakeChangeAction]
     public CombineStructureMembersOnto_Change(HashSet<Guid> membersToMerge, Guid targetLayer)
@@ -37,15 +42,46 @@ internal class CombineStructureMembersOnto_Change : Change
             if (!target.TryFindMember(guid, out var member))
                 return false;
 
-            if (member is LayerNode layer)
-                layersToCombine.Add(layer.Id);
-            else if (member is FolderNode innerFolder)
-                AddChildren(innerFolder, layersToCombine);
+            AddMember(member);
         }
 
         return true;
     }
 
+    private void AddMember(StructureNode member)
+    {
+        if (member is LayerNode layer)
+        {
+            layersToCombine.Add(layer.Id);
+        }
+        else if (member is FolderNode innerFolder)
+        {
+            layersToCombine.Add(innerFolder.Id);
+            AddChildren(innerFolder, layersToCombine);
+        }
+
+        if (member is { ClipToPreviousMember: true, Background.Connection: not null })
+        {
+            if (member.Background.Connection.Node is StructureNode structureNode)
+            {
+                AddMember(structureNode);
+            }
+            else
+            {
+                member.Background.Connection.Node.TraverseBackwards(node =>
+                {
+                    if (node is StructureNode strNode)
+                    {
+                        layersToCombine.Add(strNode.Id);
+                        return false;
+                    }
+
+                    return true;
+                });
+            }
+        }
+    }
+
     private void AddChildren(FolderNode folder, HashSet<Guid> collection)
     {
         if (folder.Content.Connection != null)
@@ -67,9 +103,8 @@ internal class CombineStructureMembersOnto_Change : Change
         out bool ignoreInUndo)
     {
         List<IChangeInfo> changes = new();
-        var targetLayer = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
+        var targetLayer = target.FindMemberOrThrow<StructureNode>(targetLayerGuid);
 
-        // TODO: add merging similar layers (vector -> vector)
         int maxFrame = GetMaxFrame(target, targetLayer);
 
         for (int frame = 0; frame < maxFrame || frame == 0; frame++)
@@ -82,7 +117,27 @@ internal class CombineStructureMembersOnto_Change : Change
         return changes;
     }
 
-    private List<IChangeInfo> ApplyToFrame(Document target, LayerNode targetLayer, int frame)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var toDrawOn = target.FindMemberOrThrow<LayerNode>(targetLayerGuid);
+
+        List<IChangeInfo> changes = new();
+
+        int maxFrame = GetMaxFrame(target, toDrawOn);
+
+        for (int frame = 0; frame < maxFrame || frame == 0; frame++)
+        {
+            changes.Add(RevertFrame(toDrawOn, frame));
+        }
+
+        target.AnimationData.RemoveKeyFrame(targetLayerGuid);
+        originalChunks.Clear();
+        changes.Add(new DeleteKeyFrame_ChangeInfo(targetLayerGuid));
+
+        return changes;
+    }
+
+    private List<IChangeInfo> ApplyToFrame(Document target, StructureNode targetLayer, int frame)
     {
         var chunksToCombine = new HashSet<VecI>();
         List<IChangeInfo> changes = new();
@@ -91,10 +146,10 @@ internal class CombineStructureMembersOnto_Change : Change
 
         foreach (var guid in ordererd)
         {
-            var layer = target.FindMemberOrThrow<LayerNode>(guid);
+            var layer = target.FindMemberOrThrow<StructureNode>(guid);
 
             AddMissingKeyFrame(targetLayer, frame, layer, changes, target);
-            
+
             if (layer is not IRasterizable or ImageLayerNode)
                 continue;
 
@@ -109,36 +164,102 @@ internal class CombineStructureMembersOnto_Change : Change
             }
         }
 
-        var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
-        toDrawOnImage.EnqueueClear();
+        bool allVector = layersToCombine.All(x => target.FindMember(x) is VectorLayerNode);
 
-        Texture tempTexture = new Texture(target.Size);
+        AffectedArea affArea = new();
 
-        DocumentRenderer renderer = new(target);
+        // TODO: add custom layer merge
+        if (!allVector)
+        {
+            affArea = RasterMerge(target, targetLayer, frame);
+        }
+        else
+        {
+            affArea = VectorMerge(target, targetLayer, frame, layersToCombine);
+        }
 
-        AffectedArea affArea = new();
-        DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
+        changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
+        return changes;
+    }
+
+    private AffectedArea VectorMerge(Document target, StructureNode targetLayer, int frame, HashSet<Guid> toCombine)
+    {
+        if (targetLayer is not VectorLayerNode vectorLayer)
+            throw new InvalidOperationException("Target layer is not a vector layer");
+
+        ShapeVectorData targetData = vectorLayer.ShapeData ?? null;
+        VectorPath? targetPath = targetData?.ToPath();
+
+        var reversed = toCombine.Reverse().ToHashSet();
+
+        foreach (var guid in reversed)
         {
-            if (frame == 0)
+            if (target.FindMember(guid) is not VectorLayerNode vectorNode)
+                continue;
+
+            if (vectorNode.ShapeData == null)
+                continue;
+
+            VectorPath path = vectorNode.ShapeData.ToPath();
+
+            if (targetData == null)
             {
-                renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full);
+                targetData = vectorNode.ShapeData;
+                targetPath = path;
+
+                if (originalPaths.ContainsKey(frame))
+                    originalPaths[frame].Dispose();
+
+                originalPaths[frame] = new VectorPath(path);
             }
             else
             {
-                HashSet<Guid> layersToRender = new();
-                foreach (var layer in layersToCombine)
-                {
-                    if (target.FindMember(layer) is LayerNode node)
-                    {
-                        if (node.KeyFrames.Any(x => x.IsInFrame(frame)))
-                        {
-                            layersToRender.Add(layer);
-                        }
-                    }
-                }
-                
-                renderer.RenderLayers(tempTexture.DrawingSurface, layersToRender, frame, ChunkResolution.Full);
+                targetPath.AddPath(path, AddPathMode.Append);
+                path.Dispose();
             }
+        }
+
+        var clone = targetData.Clone();
+        PathVectorData data;
+        if (clone is not PathVectorData vectorData)
+        {
+            ShapeVectorData shape = clone as ShapeVectorData;
+            data = new PathVectorData(targetPath)
+            {
+                StrokeColor = shape.StrokeColor,
+                FillColor = shape.FillColor,
+                StrokeWidth = shape.StrokeWidth,
+                Fill = shape.Fill,
+                TransformationMatrix = shape.TransformationMatrix,
+            };
+        }
+        else
+        {
+            data = vectorData;
+            data.Path = targetPath;
+        }
+
+        vectorLayer.ShapeData = data;
+
+        return new AffectedArea(new HashSet<VecI>());
+    }
+
+    private AffectedArea RasterMerge(Document target, StructureNode targetLayer, int frame)
+    {
+        if(targetLayer is not ImageLayerNode)
+            throw new InvalidOperationException("Target layer is not a raster layer");
+        
+        var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
+        toDrawOnImage.EnqueueClear();
+
+        Texture tempTexture = Texture.ForProcessing(target.Size, target.ProcessingColorSpace);
+
+        DocumentRenderer renderer = new(target);
+
+        AffectedArea affArea = new();
+        DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
+        {
+            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full, target.Size);
 
             toDrawOnImage.EnqueueDrawTexture(VecI.Zero, tempTexture);
 
@@ -148,11 +269,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
             tempTexture.Dispose();
         });
-
-        changes.Add(new LayerImageArea_ChangeInfo(targetLayerGuid, affArea));
-        return changes;
+        return affArea;
     }
-    
+
     private HashSet<Guid> OrderLayers(HashSet<Guid> layersToCombine, Document document)
     {
         HashSet<Guid> ordered = new();
@@ -167,7 +286,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return ordered.Reverse().ToHashSet();
     }
 
-    private void AddMissingKeyFrame(LayerNode targetLayer, int frame, LayerNode layer, List<IChangeInfo> changes,
+    private void AddMissingKeyFrame(StructureNode targetLayer, int frame, StructureNode layer, List<IChangeInfo> changes,
         Document target)
     {
         bool hasKeyframe = targetLayer.KeyFrames.Any(x => x.IsInFrame(frame));
@@ -182,21 +301,24 @@ internal class CombineStructureMembersOnto_Change : Change
             return;
 
         var clonedData = keyFrameData.Clone(true);
-        
+
         targetLayer.AddFrame(keyFrameData.KeyFrameGuid, clonedData);
-        
+
         changes.Add(new CreateRasterKeyFrame_ChangeInfo(targetLayerGuid, frame, clonedData.KeyFrameGuid, true));
         changes.Add(new KeyFrameLength_ChangeInfo(targetLayerGuid, clonedData.StartFrame, clonedData.Duration));
 
         target.AnimationData.AddKeyFrame(new RasterKeyFrame(clonedData.KeyFrameGuid, targetLayerGuid, frame, target));
     }
 
-    private int GetMaxFrame(Document target, LayerNode targetLayer)
+    private int GetMaxFrame(Document target, StructureNode targetLayer)
     {
+        if (targetLayer.KeyFrames.Count == 0)
+            return 0;
+
         int maxFrame = targetLayer.KeyFrames.Max(x => x.StartFrame + x.Duration);
         foreach (var toMerge in membersToMerge)
         {
-            var member = target.FindMemberOrThrow<LayerNode>(toMerge);
+            var member = target.FindMemberOrThrow<StructureNode>(toMerge);
             if (member.KeyFrames.Count > 0)
             {
                 maxFrame = Math.Max(maxFrame, member.KeyFrames.Max(x => x.StartFrame + x.Duration));
@@ -206,7 +328,7 @@ internal class CombineStructureMembersOnto_Change : Change
         return maxFrame;
     }
 
-    private void AddChunksByTightBounds(LayerNode layer, HashSet<VecI> chunksToCombine, int frame)
+    private void AddChunksByTightBounds(StructureNode layer, HashSet<VecI> chunksToCombine, int frame)
     {
         var tightBounds = layer.GetTightBounds(frame);
         if (tightBounds.HasValue)
@@ -224,27 +346,21 @@ internal class CombineStructureMembersOnto_Change : Change
         }
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    private IChangeInfo RevertFrame(LayerNode targetLayer, int frame)
     {
-        var toDrawOn = target.FindMemberOrThrow<ImageLayerNode>(targetLayerGuid);
-
-        List<IChangeInfo> changes = new();
-
-        int maxFrame = GetMaxFrame(target, toDrawOn);
-
-        for (int frame = 0; frame < maxFrame || frame == 0; frame++)
+        if (targetLayer is ImageLayerNode imageLayerNode)
         {
-            changes.Add(RevertFrame(toDrawOn, frame));
+            return RasterRevert(imageLayerNode, frame);
+        }
+        else if (targetLayer is VectorLayerNode vectorLayerNode)
+        {
+            return VectorRevert(vectorLayerNode, frame);
         }
-        
-        target.AnimationData.RemoveKeyFrame(targetLayerGuid);
-        originalChunks.Clear();
-        changes.Add(new DeleteKeyFrame_ChangeInfo(targetLayerGuid));
 
-        return changes;
+        throw new InvalidOperationException("Layer type not supported");
     }
 
-    private IChangeInfo RevertFrame(ImageLayerNode targetLayer, int frame)
+    private IChangeInfo RasterRevert(ImageLayerNode targetLayer, int frame)
     {
         var toDrawOnImage = targetLayer.GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
@@ -255,16 +371,34 @@ internal class CombineStructureMembersOnto_Change : Change
             DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(
                 targetLayer.GetLayerImageAtFrame(frame),
                 ref storedChunks);
-        
+
         toDrawOnImage.CommitChanges();
         return new LayerImageArea_ChangeInfo(targetLayerGuid, affectedArea);
     }
 
+    private IChangeInfo VectorRevert(VectorLayerNode targetLayer, int frame)
+    {
+        if (!originalPaths.TryGetValue(frame, out var path))
+            throw new InvalidOperationException("Original path not found");
+
+        targetLayer.ShapeData = new PathVectorData(path);
+        return new VectorShape_ChangeInfo(targetLayer.Id, new AffectedArea(new HashSet<VecI>()));
+    }
+
     public override void Dispose()
     {
         foreach (var originalChunk in originalChunks)
         {
             originalChunk.Value.Dispose();
         }
+
+        originalChunks.Clear();
+
+        foreach (var originalPath in originalPaths)
+        {
+            originalPath.Value.Dispose();
+        }
+
+        originalPaths.Clear();
     }
 }

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs

@@ -4,6 +4,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
@@ -19,10 +20,13 @@ internal class FloodFillChunkCache : IDisposable
     private readonly int frame;
 
     private readonly Dictionary<VecI, OneOf<Chunk, EmptyChunk>> acquiredChunks = new();
+    
+    private ColorSpace processingColorSpace = ColorSpace.CreateSrgbLinear();
 
     public FloodFillChunkCache(IReadOnlyChunkyImage image)
     {
         this.image = image;
+        this.processingColorSpace = image.ProcessingColorSpace;
     }
 
     public FloodFillChunkCache(HashSet<Guid> membersToRender, IReadOnlyDocument document, int frame)
@@ -30,6 +34,7 @@ internal class FloodFillChunkCache : IDisposable
         this.membersToRender = membersToRender;
         this.document = document;
         this.frame = frame;
+        processingColorSpace = document.ProcessingColorSpace;
     }
 
     public bool ChunkExistsInStorage(VecI pos)
@@ -50,14 +55,14 @@ internal class FloodFillChunkCache : IDisposable
         {
             if (document is null || membersToRender is null)
                 throw new InvalidOperationException();
-            Chunk chunk = Chunk.Create();
+            Chunk chunk = Chunk.Create(processingColorSpace);
             chunk.Surface.DrawingSurface.Canvas.Save();
             
             VecI chunkPos = pos * ChunkyImage.FullChunkSize;
             
             chunk.Surface.DrawingSurface.Canvas.Translate(-chunkPos.X, -chunkPos.Y);
             
-            document.Renderer.RenderLayers(chunk.Surface.DrawingSurface, membersToRender, frame, ChunkResolution.Full);
+            document.Renderer.RenderLayers(chunk.Surface.DrawingSurface, membersToRender, frame, ChunkResolution.Full, chunk.Surface.Size);
             
             chunk.Surface.DrawingSurface.Canvas.Restore();
             
@@ -68,7 +73,7 @@ internal class FloodFillChunkCache : IDisposable
         // there is only a single image, just get the chunk from it
         if (!image.LatestOrCommittedChunkExists(pos))
             return new EmptyChunk();
-        Chunk chunkOnImage = Chunk.Create(ChunkResolution.Full);
+        Chunk chunkOnImage = Chunk.Create(processingColorSpace, ChunkResolution.Full);
 
         if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunkOnImage.Surface.DrawingSurface, VecI.Zero, ReplacingPaint))
         {

+ 40 - 16
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -21,7 +21,8 @@ public static class FloodFillHelper
     private static readonly VecI Left = new VecI(-1, 0);
     private static readonly VecI Right = new VecI(1, 0);
 
-    internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document, int frame)
+    internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document,
+        int frame)
     {
         if (membersToFloodFill.Count == 1)
         {
@@ -33,6 +34,7 @@ public static class FloodFillHelper
                 throw new InvalidOperationException("Member is not a raster layer");
             return new FloodFillChunkCache(rasterLayer.GetLayerImageAtFrame(frame));
         }
+
         return new FloodFillChunkCache(membersToFloodFill, document, frame);
     }
 
@@ -55,12 +57,24 @@ public static class FloodFillHelper
         VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
         VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
         VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
-        Color colorToReplace = cache.GetChunk(initChunkPos).Match(
-            (Chunk chunk) => chunk.Surface.GetSRGBPixel(initPosOnChunk),
+        var chunkAtPos = cache.GetChunk(initChunkPos);
+        Color colorToReplace = chunkAtPos.Match(
+            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
-        if ((drawingColor.A == 0) || colorToReplace == drawingColor)
+        ulong uLongColor = drawingColor.ToULong();
+        Color colorSpaceCorrectedColor = drawingColor;
+        if (!document.ProcessingColorSpace.IsSrgb)
+        {
+            var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
+
+            var fixedColor = drawingColor.TransformColor(srgbTransform);
+            uLongColor = fixedColor.ToULong();
+            colorSpaceCorrectedColor = fixedColor;
+        }
+
+        if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
             return new();
 
         RectI globalSelectionBounds = (RectI?)selection?.TightBounds ?? new RectI(VecI.Zero, document.Size);
@@ -68,7 +82,6 @@ public static class FloodFillHelper
         // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.
         // Used for faster pixel checking
         ColorBounds colorRange = new(colorToReplace, tolerance);
-        ulong uLongColor = drawingColor.ToULong();
 
         Dictionary<VecI, Chunk> drawingChunks = new();
         HashSet<VecI> processedEmptyChunks = new();
@@ -84,10 +97,11 @@ public static class FloodFillHelper
 
             if (!drawingChunks.ContainsKey(chunkPos))
             {
-                var chunk = Chunk.Create();
+                var chunk = Chunk.Create(document.ProcessingColorSpace);
                 chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
                 drawingChunks[chunkPos] = chunk;
             }
+
             var drawingChunk = drawingChunks[chunkPos];
             var referenceChunk = cache.GetChunk(chunkPos);
 
@@ -108,8 +122,10 @@ public static class FloodFillHelper
                         if (chunkPos.X < imageSizeInChunks.X - 1)
                             positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
                     }
+
                     processedEmptyChunks.Add(chunkPos);
                 }
+
                 continue;
             }
 
@@ -123,7 +139,7 @@ public static class FloodFillHelper
                 chunkPos,
                 chunkSize,
                 uLongColor,
-                drawingColor,
+                colorSpaceCorrectedColor,
                 posOnChunk,
                 colorRange,
                 iter != 0);
@@ -142,6 +158,7 @@ public static class FloodFillHelper
                     positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
             }
         }
+
         return drawingChunks;
     }
 
@@ -158,9 +175,10 @@ public static class FloodFillHelper
         ColorBounds bounds,
         bool checkFirstPixel)
     {
-        if (referenceChunk.Surface.GetSRGBPixel(pos) == color || drawingChunk.Surface.GetSRGBPixel(pos) == color)
+        // color should be a fixed color
+        if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
             return null;
-        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetSRGBPixel(pos)))
+        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
             return null;
 
         byte[] pixelStates = new byte[chunkSize * chunkSize];
@@ -186,13 +204,17 @@ public static class FloodFillHelper
 
             if (curPos.X > 0 && pixelStates[pixelOffset - 1] == InSelection && bounds.IsWithinBounds(refPixel - 4))
                 toVisit.Push(new(curPos.X - 1, curPos.Y));
-            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection && bounds.IsWithinBounds(refPixel + 4))
+            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection &&
+                bounds.IsWithinBounds(refPixel + 4))
                 toVisit.Push(new(curPos.X + 1, curPos.Y));
-            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection && bounds.IsWithinBounds(refPixel - 4 * chunkSize))
+            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection &&
+                bounds.IsWithinBounds(refPixel - 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y - 1));
-            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection && bounds.IsWithinBounds(refPixel + 4 * chunkSize))
+            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection &&
+                bounds.IsWithinBounds(refPixel + 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y + 1));
         }
+
         return pixelStates;
     }
 
@@ -201,7 +223,7 @@ public static class FloodFillHelper
         Surface surface = new Surface(document.Size);
 
         var inverse = new VectorPath();
-        inverse.AddRect(new RectI(new(0, 0), document.Size));
+        inverse.AddRect((RectD)new RectI(new(0, 0), document.Size));
 
         surface.DrawingSurface.Canvas.Clear(new Color(255, 255, 255, 255));
         surface.DrawingSurface.Canvas.Flush();
@@ -215,12 +237,13 @@ public static class FloodFillHelper
     /// <summary>
     /// Use skia to set all pixels in array that are inside selection to InSelection
     /// </summary>
-    private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos, int chunkSize)
+    private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos,
+        int chunkSize)
     {
         if (selection is null)
         {
             selection = new VectorPath();
-            selection.AddRect(globalBounds);
+            selection.AddRect((RectD)globalBounds);
         }
 
         RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
@@ -232,7 +255,8 @@ public static class FloodFillHelper
         fixed (byte* arr = array)
         {
             using DrawingSurface drawingSurface = DrawingSurface.Create(
-                new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr, chunkSize);
+                new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr,
+                chunkSize);
             drawingSurface.Canvas.ClipPath(shiftedSelection);
             drawingSurface.Canvas.Clear(new Color(InSelection, InSelection, InSelection));
             drawingSurface.Canvas.Flush();

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

@@ -42,6 +42,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.spacing = spacing;
         points.Add(pos);
         this.frame = frame;
+        
+        srcPaint.Shader?.Dispose();
+        srcPaint.Shader = null;
+        
         if (this.antiAliasing && !erasing)
         {
             srcPaint.BlendMode = BlendMode.SrcOver;
@@ -82,7 +86,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         int opCount = image.QueueLength;
 
         var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-        
+
         float spacingPixels = strokeWidth * spacing;
 
         foreach (var point in bresenham)
@@ -96,7 +100,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             {
                 ApplySoftnessGradient((VecD)point);
             }
-            
+
             image.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
         }
 
@@ -110,12 +114,17 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         if (points.Count == 1)
         {
             var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            targetImage.EnqueueDrawEllipse((RectD)rect, color, color, 1, 0, antiAliasing, srcPaint);
+            if (antiAliasing)
+            {
+                ApplySoftnessGradient(points[0]);
+            }
+
+            targetImage.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
             return;
         }
 
         VecF lastPos = points[0];
-        
+
         float spacingInPixels = strokeWidth * this.spacing;
 
         for (int i = 0; i < points.Count; i++)
@@ -142,7 +151,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         radius = MathF.Max(1, radius);
         srcPaint.Shader = Shader.CreateRadialGradient(
             pos, radius, [color, color.WithAlpha(0)],
-            [hardness - 0.04f, 1f], ShaderTileMode.Clamp);
+            [Math.Max(hardness - 0.05f, 0), 0.95f], ShaderTileMode.Clamp);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -140,14 +140,14 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     {
         ChunkyImage image =
             DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
-        VectorPath pathToExtract = originalPath;
+        VectorPath? pathToExtract = originalPath;
         RectD targetBounds = originalTightBounds;
 
         if (pathToExtract == null)
         {
             RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
             pathToExtract = new VectorPath();
-            pathToExtract.AddRect((RectI)tightBounds);
+            pathToExtract.AddRect(tightBounds.RoundOutwards());
         }
 
         member.OriginalPath = pathToExtract;

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectionsData.cs

@@ -10,4 +10,50 @@ public class ConnectionsData
         this.originalOutputConnections = originalOutputConnections;
         this.originalInputConnections = originalInputConnections;
     }
+
+    public ConnectionsData WithUpdatedIds(Dictionary<Guid,Guid> nodeMap)
+    {
+        Dictionary<PropertyConnection, List<PropertyConnection>> newOutputConnections = new();
+        foreach (var (key, value) in originalOutputConnections)
+        {
+            Guid? sourceNodeId = key.NodeId;
+            if (sourceNodeId.HasValue)
+            {
+                sourceNodeId = nodeMap[sourceNodeId.Value];
+            }
+            
+            var valueCopy = new List<PropertyConnection>();
+            foreach (var connection in value)
+            {
+                Guid? targetNodeId = connection.NodeId;
+                if (targetNodeId.HasValue)
+                {
+                    targetNodeId = nodeMap[targetNodeId.Value];
+                }
+                valueCopy.Add(connection with { NodeId = targetNodeId });
+            }
+            
+            newOutputConnections.Add(key with { NodeId = sourceNodeId }, valueCopy);
+        }
+        
+        List<(PropertyConnection, PropertyConnection?)> newInputConnections = new();
+        foreach (var (input, output) in originalInputConnections)
+        {
+            Guid? inputNodeId = input.NodeId;
+            if (inputNodeId.HasValue)
+            {
+                inputNodeId = nodeMap[inputNodeId.Value];
+            }
+            
+            Guid? outputNodeId = output?.NodeId;
+            if (outputNodeId.HasValue)
+            {
+                outputNodeId = nodeMap[outputNodeId.Value];
+            }
+            
+            newInputConnections.Add((input with { NodeId = inputNodeId }, new PropertyConnection(outputNodeId, output?.PropertyName)));
+        }
+        
+        return new ConnectionsData(newOutputConnections, newInputConnections);
+    }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeleteNode_Change.cs

@@ -94,6 +94,7 @@ internal class DeleteNode_Change : Change
 
         changes.AddRange(NodeOperations.CreateUpdateInputs(copy));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, copy, doc.NodeGraph));
+        changes.Add(new NodePosition_ChangeInfo(copy.Id, copy.Position));
 
         RevertKeyFrames(doc, savedKeyFrameGroup, changes);
 

+ 47 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DuplicateNode_Change.cs

@@ -0,0 +1,47 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class DuplicateNode_Change : Change
+{
+    private Guid nodeGuid;
+    
+    private Guid createdNodeGuid;
+
+    [GenerateMakeChangeAction]
+    public DuplicateNode_Change(Guid nodeGuid, Guid newGuid)
+    {
+        this.nodeGuid = nodeGuid;
+        createdNodeGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindNode(nodeGuid, out Node node) && node.GetNodeTypeUniqueName() != OutputNode.UniqueName;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        Node existingNode = target.FindNode(nodeGuid);
+        Node clone = existingNode.Clone();
+        clone.Id = createdNodeGuid;
+
+        target.NodeGraph.AddNode(clone);
+
+        ignoreInUndo = false;
+
+        return CreateNode_ChangeInfo.CreateFromNode(clone);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.FindNode(createdNodeGuid);
+        target.NodeGraph.RemoveNode(node);
+        
+        node.Dispose();
+
+        return new DeleteNode_ChangeInfo(node.Id);
+    }
+}

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

@@ -94,7 +94,7 @@ public static class NodeOperations
 
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
-
+        
         return changes;
     }
 
@@ -133,7 +133,7 @@ public static class NodeOperations
         return changes;
     }
 
-    public static ConnectionsData CreateConnectionsData(Node node)
+    public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
 

+ 65 - 0
src/PixiEditor.ChangeableDocument/Changes/Properties/ChangeProcessingColorSpace_Change.cs

@@ -0,0 +1,65 @@
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+
+namespace PixiEditor.ChangeableDocument.Changes.Properties;
+
+internal class ChangeProcessingColorSpace_Change : Change
+{
+    private ColorSpace toColorSpace;
+    private ColorSpace original;
+
+    [GenerateMakeChangeAction]
+    public ChangeProcessingColorSpace_Change(ColorSpace newColorSpace)
+    {
+        this.toColorSpace = newColorSpace;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        original = target.ProcessingColorSpace;
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        target.ProcessingColorSpace = toColorSpace;
+
+        ConvertImageNodes(target, toColorSpace);
+
+        return new ProcessingColorSpace_ChangeInfo(toColorSpace);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        target.ProcessingColorSpace = original;
+
+        ConvertImageNodes(target, original);
+
+        return new ProcessingColorSpace_ChangeInfo(original);
+    }
+
+    private void ConvertImageNodes(Document target, ColorSpace newColorSpace)
+    {
+        foreach (var node in target.NodeGraph.Nodes)
+        {
+            if (node is ImageLayerNode imageLayerNode)
+            {
+                foreach (var keyFrame in imageLayerNode.KeyFrames)
+                {
+                    if (keyFrame.Data is ChunkyImage chunkyImage)
+                    {
+                        ChunkyImage img = new ChunkyImage(chunkyImage.LatestSize, newColorSpace);
+                        img.EnqueueDrawCommitedChunkyImage(VecI.Zero, chunkyImage);
+                        img.CommitChanges();
+
+                        keyFrame.Data = img;
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs

@@ -22,7 +22,7 @@ internal class CreateStructureMemberMask_Change : Change
         var member = target.FindMemberOrThrow(targetMember);
         if (member.EmbeddedMask is not null)
             throw new InvalidOperationException("Cannot create a mask; the target member already has one");
-        member.EmbeddedMask = new ChunkyImage(target.Size);
+        member.EmbeddedMask = new ChunkyImage(target.Size, target.ProcessingColorSpace);
 
         ignoreInUndo = false;
         return new StructureMemberMask_ChangeInfo(targetMember, true);

+ 17 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -37,10 +37,22 @@ internal sealed class RotateImage_Change : Change
         {
             membersToRotate = target.ExtractLayers(membersToRotate);
 
+            RectD? bounds = null;
             foreach (var layer in membersToRotate)
             {
                 if (!target.HasMember(layer)) return false;
+
+                if (frame != null)
+                {
+                    var layerBounds = target.FindMember(layer).GetTightBounds(frame.Value);
+                    if (layerBounds.HasValue)
+                    {
+                        bounds = bounds?.Union(layerBounds.Value) ?? layerBounds.Value;
+                    }
+                }
             }
+            
+            if(frame != null && (bounds == null || bounds.Value.IsZeroArea)) return false;
         }
 
         originalSize = target.Size;
@@ -70,6 +82,11 @@ internal sealed class RotateImage_Change : Change
                 bounds = preciseBounds.Value;
             }
         }
+        
+        if (bounds.IsZeroArea)
+        {
+            return;
+        }
 
         int originalWidth = bounds.Width;
         int originalHeight = bounds.Height;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -126,7 +126,7 @@ internal class MagicWandHelper
 
 
         Color colorToReplace = cache.GetChunk(initChunkPos).Match(
-            (Chunk chunk) => chunk.Surface.GetSRGBPixel(initPosOnChunk),
+            (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
 
@@ -257,7 +257,7 @@ internal class MagicWandHelper
         VecI pos,
         ColorBounds bounds, Lines lines)
     {
-        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetSRGBPixel(pos)))
+        if (!bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
         {
             return null;
         }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectEllipse_UpdateableChange.cs

@@ -29,7 +29,7 @@ internal class SelectEllipse_UpdateableChange : UpdateableChange
     {
         originalPath = new VectorPath(target.Selection.SelectionPath);
         documentConstraint = new VectorPath();
-        documentConstraint.AddRect(new RectI(VecI.Zero, target.Size));
+        documentConstraint.AddRect((RectD)new RectI(VecI.Zero, target.Size));
         return true;
     }
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -27,7 +27,7 @@ internal class CreateStructureMember_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if(structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+        if(structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
             return false;
         
         return target.TryFindNode<Node>(parentGuid, out _);

+ 139 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -0,0 +1,139 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class DuplicateFolder_Change : Change
+{
+    private readonly Guid folderGuid;
+    private Guid duplicateGuid;
+    private Guid[] contentGuids;
+    private Guid[] contentDuplicateGuids;
+
+    private ConnectionsData? connectionsData;
+    private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+
+    [GenerateMakeChangeAction]
+    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid)
+    {
+        this.folderGuid = folderGuid;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (!target.TryFindMember<FolderNode>(folderGuid, out FolderNode? folder))
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(folder);
+
+        List<Guid> contentGuidList = new();
+
+        folder.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            contentGuidList.Add(x.Id);
+            contentConnectionsData[x.Id] = NodeOperations.CreateConnectionsData(x);
+            return true;
+        });
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        (FolderNode existingLayer, Node parent) = ((FolderNode, Node))target.FindChildAndParentOrThrow(folderGuid);
+
+        FolderNode clone = (FolderNode)existingLayer.Clone();
+        clone.Id = duplicateGuid;
+
+        InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter) &&
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+        
+        operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+
+        DuplicateContent(target, clone, existingLayer, operations);
+        
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (contentDuplicateGuids is not null)
+        {
+            foreach (Guid contentGuid in contentDuplicateGuids)
+            {
+                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
+                changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
+                
+                target.NodeGraph.RemoveNode(contentNode);
+                contentNode.Dispose();
+            }
+        }
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.FindNodeOrThrow<Node>(folderGuid);
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        return changes;
+    }
+
+    private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
+        List<IChangeInfo> operations)
+    {
+        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+
+        nodeMap[existingLayer.Id] = clone.Id;
+        List<Guid> contentGuidList = new();
+
+        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            if (x is not Node targetNode)
+                return false;
+
+            Node? node = targetNode.Clone();
+            nodeMap[x.Id] = node.Id;
+            contentGuidList.Add(node.Id);
+
+            target.NodeGraph.AddNode(node);
+
+            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+            return true;
+        });
+
+        foreach (var data in contentConnectionsData)
+        {
+            var updatedData = data.Value.WithUpdatedIds(nodeMap);
+            Guid targetNodeId = nodeMap[data.Key];
+            operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
+                target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
+        }
+        
+        contentDuplicateGuids = contentGuidList.ToArray();
+    }
+}

+ 3 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
@@ -40,14 +41,14 @@ internal class DuplicateLayer_Change : Change
 
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter) &&
-            x.Connection.Node is StructureNode) as InputProperty<Painter?>;
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
 
         List<IChangeInfo> operations = new();
 
         target.NodeGraph.AddNode(clone);
 
         operations.Add(CreateLayer_ChangeInfo.FromLayer(clone));
-
+        
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
         ignoreInUndo = false;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -44,7 +44,7 @@ internal class RasterizeMember_Change : Change
         
         IRasterizable rasterizable = (IRasterizable)node;
         
-        ImageLayerNode imageLayer = new ImageLayerNode(target.Size);
+        ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         imageLayer.MemberName = node.DisplayName;
 
         target.NodeGraph.AddNode(imageLayer);

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

@@ -96,4 +96,14 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
 
         return new VectorShape_ChangeInfo(node.Id, affected);
     }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        if (other is SetShapeGeometry_UpdateableChange change)
+        {
+            return change.TargetId == TargetId;
+        }
+
+        return false;
+    }
 }

+ 173 - 27
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -1,9 +1,11 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System.Collections.Concurrent;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -18,6 +20,8 @@ public class DocumentRenderer : IPreviewRenderable
         BlendMode = BlendMode.Src, Color = Drawie.Backend.Core.ColorsImpl.Colors.Transparent
     };
 
+    private Texture renderTexture;
+
     public DocumentRenderer(IReadOnlyDocument document)
     {
         Document = document;
@@ -34,7 +38,7 @@ public class DocumentRenderer : IPreviewRenderable
             {
                 if (node is IChunkRenderable imageNode)
                 {
-                    imageNode.RenderChunk(chunkPos, resolution, frameTime);
+                    imageNode.RenderChunk(chunkPos, resolution, frameTime, Document.ProcessingColorSpace);
                 }
             }));
         }
@@ -43,28 +47,47 @@ public class DocumentRenderer : IPreviewRenderable
         }
     }
 
-    public void RenderLayers(DrawingSurface toDrawOn, HashSet<Guid> layersToCombine, int frame,
-        ChunkResolution resolution)
+    public void RenderLayers(DrawingSurface toRenderOn, HashSet<Guid> layersToCombine, int frame,
+        ChunkResolution resolution, VecI renderSize)
     {
         IsBusy = true;
-        RenderContext context = new(toDrawOn, frame, resolution, Document.Size);
+
+        if (renderTexture == null || renderTexture.Size != renderSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(renderSize, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Save();
+        renderTexture.DrawingSurface.Canvas.Clear();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
+        toRenderOn.Canvas.Save();
+        toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+
+        RenderContext context = new(renderTexture.DrawingSurface, frame, resolution, Document.Size,
+            Document.ProcessingColorSpace);
         context.FullRerender = true;
         IReadOnlyNodeGraph membersOnlyGraph = ConstructMembersOnlyGraph(layersToCombine, Document.NodeGraph);
         try
         {
             membersOnlyGraph.Execute(context);
+            toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
         }
         catch (ObjectDisposedException)
         {
         }
         finally
         {
+            renderTexture.DrawingSurface.Canvas.Restore();
+            toRenderOn.Canvas.Restore();
             IsBusy = false;
         }
     }
 
 
-    public void RenderLayer(DrawingSurface renderOn, Guid layerId, ChunkResolution resolution, KeyFrameTime frameTime)
+    public void RenderLayer(DrawingSurface toRenderOn, Guid layerId, ChunkResolution resolution, KeyFrameTime frameTime,
+        VecI renderSize)
     {
         var node = Document.FindMember(layerId);
 
@@ -72,13 +95,47 @@ public class DocumentRenderer : IPreviewRenderable
         {
             return;
         }
-        
+
         IsBusy = true;
 
-        RenderContext context = new(renderOn, frameTime, resolution, Document.Size);
+        if (renderTexture == null || renderTexture.Size != renderSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(renderSize, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Save();
+        renderTexture.DrawingSurface.Canvas.Clear();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
+        toRenderOn.Canvas.Save();
+        toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+
+        RenderContext context = new(renderTexture.DrawingSurface, frameTime, resolution, Document.Size, Document.ProcessingColorSpace);
         context.FullRerender = true;
+
+        node.RenderForOutput(context, toRenderOn, null);
+        
+        renderTexture.DrawingSurface.Canvas.Restore();
+        toRenderOn.Canvas.Restore();
         
-        node.RenderForOutput(context, renderOn, null);
+        IsBusy = false;
+    }
+
+    public void RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn, RenderContext context,
+        string elementToRenderName)
+    {
+        if (IsBusy)
+        {
+            return;
+        }
+
+        IsBusy = true;
+
+        if (previewRenderable is Node { IsDisposed: true }) return;
+
+        previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
+
         IsBusy = false;
     }
 
@@ -87,7 +144,8 @@ public class DocumentRenderer : IPreviewRenderable
         return ConstructMembersOnlyGraph(null, fullGraph);
     }
 
-    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(HashSet<Guid>? layersToCombine,
+    public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(
+        HashSet<Guid>? membersToCombine,
         IReadOnlyNodeGraph fullGraph)
     {
         NodeGraph membersOnlyGraph = new();
@@ -96,26 +154,40 @@ public class DocumentRenderer : IPreviewRenderable
 
         membersOnlyGraph.AddNode(outputNode);
 
-        List<LayerNode> layersInOrder = new();
+        Dictionary<Guid, Guid> nodeMapping = new();
 
-        fullGraph.TryTraverse(node =>
+        fullGraph.OutputNode.TraverseBackwards((node, input) =>
         {
-            if (node is LayerNode layer && (layersToCombine == null || layersToCombine.Contains(layer.Id)))
+            if (node is StructureNode structureNode && membersToCombine != null &&
+                !membersToCombine.Contains(structureNode.Id))
             {
-                layersInOrder.Insert(0, layer);
+                return true;
             }
-        });
 
-        IInputProperty<Painter> lastInput = outputNode.Input;
+            if (node is LayerNode layer)
+            {
+                LayerNode clone = (LayerNode)layer.Clone();
+                membersOnlyGraph.AddNode(clone);
 
-        foreach (var layer in layersInOrder)
-        {
-            var clone = (LayerNode)layer.Clone();
-            membersOnlyGraph.AddNode(clone);
 
-            clone.Output.ConnectTo(lastInput);
-            lastInput = clone.Background;
-        }
+                IInputProperty targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+
+                clone.Output.ConnectTo(targetInput);
+                nodeMapping[layer.Id] = clone.Id;
+            }
+            else if (node is FolderNode folder)
+            {
+                FolderNode clone = (FolderNode)folder.Clone();
+                membersOnlyGraph.AddNode(clone);
+
+                var targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+
+                clone.Output.ConnectTo(targetInput);
+                nodeMapping[folder.Id] = clone.Id;
+            }
+
+            return true;
+        });
 
         return membersOnlyGraph;
     }
@@ -123,20 +195,94 @@ public class DocumentRenderer : IPreviewRenderable
     public RectD? GetPreviewBounds(int frame, string elementNameToRender = "") =>
         new(0, 0, Document.Size.X, Document.Size.Y);
 
-    public bool RenderPreview(DrawingSurface renderOn, ChunkResolution resolution, int frame,
+    public bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
     {
-        RenderContext context = new(renderOn, frame, resolution, Document.Size);
+        IsBusy = true;
+
+        if (renderTexture == null || renderTexture.Size != Document.Size)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(Document.Size, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Clear();
+        context.RenderSurface = renderTexture.DrawingSurface;
         Document.NodeGraph.Execute(context);
 
+        renderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
+
+        IsBusy = false;
+
         return true;
     }
 
-    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime)
+    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize)
     {
         IsBusy = true;
-        RenderContext context = new(toRenderOn, frameTime, ChunkResolution.Full, Document.Size) { FullRerender = true };
+
+        if (renderTexture == null || renderTexture.Size != renderSize)
+        {
+            renderTexture?.Dispose();
+            renderTexture = Texture.ForProcessing(renderSize, Document.ProcessingColorSpace);
+        }
+
+        renderTexture.DrawingSurface.Canvas.Save();
+        renderTexture.DrawingSurface.Canvas.Clear();
+
+        renderTexture.DrawingSurface.Canvas.SetMatrix(toRenderOn.Canvas.TotalMatrix);
+        toRenderOn.Canvas.Save();
+        toRenderOn.Canvas.SetMatrix(Matrix3X3.Identity);
+
+        RenderContext context =
+            new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size,
+                Document.ProcessingColorSpace) { FullRerender = true };
         Document.NodeGraph.Execute(context);
+
+        toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
+
+        renderTexture.DrawingSurface.Canvas.Restore();
+        toRenderOn.Canvas.Restore();
+
         IsBusy = false;
     }
+
+    private static IInputProperty GetTargetInput(IInputProperty? input,
+        IReadOnlyNodeGraph sourceGraph,
+        NodeGraph membersOnlyGraph,
+        Dictionary<Guid, Guid> nodeMapping)
+    {
+        if (input == null)
+        {
+            if (membersOnlyGraph.OutputNode is IRenderInput inputNode) return inputNode.Background;
+
+            return null;
+        }
+
+        if (nodeMapping.ContainsKey(input.Node?.Id ?? Guid.Empty))
+        {
+            return membersOnlyGraph.Nodes.First(x => x.Id == nodeMapping[input.Node.Id])
+                .GetInputProperty(input.InternalPropertyName);
+        }
+
+        var sourceNode = sourceGraph.AllNodes.First(x => x.Id == input.Node.Id);
+
+        IInputProperty? found = null;
+        sourceNode.TraverseForwards((n, input) =>
+        {
+            if (n is StructureNode structureNode)
+            {
+                if (nodeMapping.TryGetValue(structureNode.Id, out var value))
+                {
+                    Node mappedNode = membersOnlyGraph.Nodes.First(x => x.Id == value);
+                    found = mappedNode.GetInputProperty(input.InternalPropertyName);
+                    return false;
+                }
+            }
+
+            return true;
+        });
+
+        return found ?? (membersOnlyGraph.OutputNode as IRenderInput)?.Background;
+    }
 }

+ 6 - 1
src/PixiEditor.ChangeableDocument/Rendering/RenderContext.cs

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using DrawingApiBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
@@ -16,16 +17,20 @@ public class RenderContext
     
     public DrawingSurface RenderSurface { get; set; }
     public bool FullRerender { get; set; } = false;
+    
+    public ColorSpace ProcessingColorSpace { get; set; }
+    public string? TargetOutput { get; set; }   
 
 
     public RenderContext(DrawingSurface renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution,
-        VecI docSize, double opacity = 1) 
+        VecI docSize, ColorSpace processingColorSpace, double opacity = 1) 
     {
         RenderSurface = renderSurface;
         FrameTime = frameTime;
         ChunkResolution = chunkResolution;
         DocumentSize = docSize;
         Opacity = opacity;
+        ProcessingColorSpace = processingColorSpace;
     }
 
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 1 - 0
src/PixiEditor.Extensions/UI/Overlays/IOverlay.cs

@@ -4,6 +4,7 @@ using Drawie.Numerics;
 namespace PixiEditor.Extensions.UI.Overlays;
 
 public delegate void PointerEvent(OverlayPointerArgs args);
+public delegate void KeyEvent(Key key, KeyModifiers modifiers);
 public interface IOverlay
 {
     public void EnterPointer(OverlayPointerArgs args);

+ 12 - 0
src/PixiEditor.SVG/Attributes/SvgValueAttribute.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.SVG.Attributes;
+
+[AttributeUsage(AttributeTargets.Field)]
+public class SvgValueAttribute : Attribute
+{
+    public string Value { get; }
+
+    public SvgValueAttribute(string value)
+    {
+        Value = value;
+    }
+}

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

@@ -8,4 +8,10 @@ public class SvgCircle() : SvgPrimitive("circle")
     public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy");
 
     public SvgProperty<SvgNumericUnit> R { get; } = new("r");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return Cx;
+        yield return Cy;
+        yield return R;
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgEllipse.cs

@@ -9,5 +9,12 @@ public class SvgEllipse() : SvgPrimitive("ellipse")
     public SvgProperty<SvgNumericUnit> Cy { get; } = new("cy"); 
     
     public SvgProperty<SvgNumericUnit> Rx { get; } = new("rx"); 
-    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry"); 
+    public SvgProperty<SvgNumericUnit> Ry { get; } = new("ry");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return Cx;
+        yield return Cy;
+        yield return Rx;
+        yield return Ry;
+    }
 }

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

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

+ 9 - 2
src/PixiEditor.SVG/Elements/SvgImage.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Enums;
+using System.Xml;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
@@ -11,7 +12,7 @@ public class SvgImage : SvgElement
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
         
-    public SvgProperty<SvgStringUnit> Href { get; } = new("xlink:href");
+    public SvgProperty<SvgStringUnit> Href { get; } = new("href", "xlink");
     public SvgProperty<SvgLinkUnit> Mask { get; } = new("mask");
     public SvgProperty<SvgEnumUnit<SvgImageRenderingType>> ImageRendering { get; } = new("image-rendering");
 
@@ -19,4 +20,10 @@ public class SvgImage : SvgElement
     {
         RequiredNamespaces.Add("xlink", "http://www.w3.org/1999/xlink");
     }
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { X, Y, Width, Height, Href, Mask, ImageRendering };
+        ParseAttributes(properties, reader);
+    }
 }

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

@@ -9,4 +9,11 @@ public class SvgLine() : SvgPrimitive("line")
     
     public SvgProperty<SvgNumericUnit> X2 { get; } = new("x2");
     public SvgProperty<SvgNumericUnit> Y2 { get; } = new("y2");
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return X1;
+        yield return Y1;
+        yield return X2;
+        yield return Y2;
+    }
 }

+ 8 - 1
src/PixiEditor.SVG/Elements/SvgMask.cs

@@ -1,4 +1,5 @@
-using PixiEditor.SVG.Features;
+using System.Xml;
+using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
@@ -11,4 +12,10 @@ public class SvgMask() : SvgElement("mask"), IElementContainer
     public SvgProperty<SvgNumericUnit> Width { get; } = new("width");
     public SvgProperty<SvgNumericUnit> Height { get; } = new("height");
     public List<SvgElement> Children { get; } = new();
+
+    public override void ParseData(XmlReader reader)
+    {
+        List<SvgProperty> properties = new List<SvgProperty>() { X, Y, Width, Height };
+        ParseAttributes(properties, reader);
+    }
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików