Parcourir la source

Merge branch 'master' into pattern-node

Krzysztof Krysiński il y a 1 semaine
Parent
commit
7138217c19
100 fichiers modifiés avec 3283 ajouts et 598 suppressions
  1. 1 1
      README.md
  2. 1 1
      samples/Directory.Build.props
  3. 3 3
      src/ChunkyImageLib/Chunk.cs
  4. 182 31
      src/ChunkyImageLib/ChunkyImage.cs
  5. 17 17
      src/ChunkyImageLib/ChunkyImageEx.cs
  6. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  7. 3 3
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  8. 1 1
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  9. 4 4
      src/ChunkyImageLib/Operations/BresenhamLineHelper.cs
  10. 5 5
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  11. 9 3
      src/ChunkyImageLib/Operations/ImageOperation.cs
  12. 24 0
      src/ChunkyImageLib/Operations/LineHelper.cs
  13. 53 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  14. 3 24
      src/ChunkyImageLib/Operations/PixelOperation.cs
  15. 6 1
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  16. 1 1
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  17. 3 1
      src/ChunkyImageLib/Operations/TextureOperation.cs
  18. 1 1
      src/ColorPicker
  19. 1 1
      src/Directory.Build.props
  20. 1 1
      src/PixiDocks
  21. 1 1
      src/PixiEditor.Api.CGlueMSBuild/PixiEditor.Api.CGlueMSBuild.csproj
  22. 6 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableExposed_ChangeInfo.cs
  23. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableRemoved_ChangeInfo.cs
  24. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariable_ChangeInfo.cs
  25. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/RenameBlackboardVariable_ChangeInfo.cs
  26. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  28. 4 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/NestedDocumentLink_ChangeInfo.cs
  29. 17 0
      src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs
  30. 19 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushData.cs
  31. 614 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs
  32. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/IBrush.cs
  33. 20 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/RecordedPoint.cs
  34. 49 6
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  35. 16 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentGraphPipe.cs
  36. 22 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentReference.cs
  37. 119 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Blackboard.cs
  38. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/BrushRenderContext.cs
  39. 39 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  40. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/IReadOnlyBlackboard.cs
  41. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  42. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IClipSource.cs
  43. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  44. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNodeGraph.cs
  45. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  46. 8 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  47. 94 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  48. 5 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  49. 54 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/BlackboardVariableValueNode.cs
  50. 357 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs
  51. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/IBrushSampleTextureNode.cs
  52. 50 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/StrokeInfoNode.cs
  53. 12 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  54. 19 17
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  55. 8 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  56. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Editor/EditorInfoNode.cs
  57. 7 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  58. 6 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  59. 20 20
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  60. 13 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs
  61. 66 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  62. 144 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/GradientNode.cs
  63. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/MaskNode.cs
  64. 18 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  65. 33 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KeyboardInfoNode.cs
  66. 45 30
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  67. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  68. 21 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  69. 7 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  70. 9 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  71. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  72. 479 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NestedDocumentNode.cs
  73. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  74. 7 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  75. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  76. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Painter.cs
  77. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/PointerInfoNode.cs
  78. 17 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  79. 14 14
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  80. 32 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  81. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  82. 53 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/PixelPerfectEllipseNode.cs
  83. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  84. 0 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  85. 18 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  86. 13 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs
  87. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  88. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/EqualsNode.cs
  89. 41 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/SwitchNode.cs
  90. 13 15
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  91. 36 26
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  92. 26 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ExposeValueNode.cs
  93. 38 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ViewportInfoNode.cs
  94. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IInputDependentOutputs.cs
  95. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IRasterizable.cs
  96. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyAnimationData.cs
  97. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  98. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IVariableSampling.cs
  99. 0 186
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  100. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs

+ 1 - 1
README.md

@@ -8,7 +8,7 @@
 
 <img width="50%" align="center" src="https://github.com/user-attachments/assets/bd08c8bd-f610-449d-b1e2-6a990e562518">
 
-## The only 2D Graphics Editor you'll ever need
+## The All-in-One Editor for 2D
 
 **PixiEditor** is a universal 2D editor that was made to provide you with tools and features for all your 2D needs. Create beautiful sprites for your games, animations, edit images, create logos. All packed up in an intuitive and familiar interface.
 

+ 1 - 1
samples/Directory.Build.props

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

+ 3 - 3
src/ChunkyImageLib/Chunk.cs

@@ -87,17 +87,17 @@ public class Chunk : IDisposable
     /// </summary>
     /// <param name="pos">The destination for the <paramref name="surface"/></param>
     /// <param name="paint">The paint to use while drawing</param>
-    public void DrawChunkOn(DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? samplingOptions = null)
+    public void DrawChunkOn(Canvas surface, VecD pos, Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
         using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         if (samplingOptions == null || samplingOptions == SamplingOptions.Default)
         {
-            surface.Canvas.DrawSurface(Surface.DrawingSurface, (float)pos.X, (float)pos.Y, paint);
+            surface.DrawSurface(Surface.DrawingSurface, (float)pos.X, (float)pos.Y, paint);
         }
         else
         {
             using var snapshot = Surface.DrawingSurface.Snapshot();
-            surface.Canvas.DrawImage(snapshot, (float)pos.X, (float)pos.Y, samplingOptions.Value, paint);
+            surface.DrawImage(snapshot, (float)pos.X, (float)pos.Y, samplingOptions.Value, paint);
         }
     }
 

+ 182 - 31
src/ChunkyImageLib/ChunkyImage.cs

@@ -11,6 +11,7 @@ using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -64,8 +65,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private readonly object lockObject = new();
     private int commitCounter = 0;
 
-    private RectI cachedPreciseBounds = RectI.Empty;
-    private int lastBoundsCacheHash = -1;
+    private RectI cachedPreciseCommitedBounds = RectI.Empty;
+    private RectI cachedPreciseLatestBounds = RectI.Empty;
+    private int lastCommitedBoundsCacheHash = -1;
+    private int lastLatestBoundsCacheHash = -1;
 
     public const int FullChunkSize = ChunkPool.FullChunkSize;
     private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
@@ -200,9 +203,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         {
             ThrowIfDisposed();
 
-            if (lastBoundsCacheHash == GetCacheHash())
+            if (lastCommitedBoundsCacheHash == GetCacheHash())
             {
-                return cachedPreciseBounds;
+                return cachedPreciseCommitedBounds;
             }
 
             var chunkSize = suggestedResolution.PixelSize();
@@ -250,8 +253,120 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
             preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
 
-            cachedPreciseBounds = preciseBounds.GetValueOrDefault();
-            lastBoundsCacheHash = GetCacheHash();
+            cachedPreciseCommitedBounds = preciseBounds.GetValueOrDefault();
+            lastCommitedBoundsCacheHash = GetCacheHash();
+
+            return preciseBounds;
+        }
+    }
+
+    public RectI? FindTightLatestBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
+        bool fallbackToChunkAligned = false)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+
+            if(queuedOperations.Count == 0)
+            {
+                return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
+            }
+
+            if (lastLatestBoundsCacheHash == GetCacheHash())
+            {
+                return cachedPreciseLatestBounds;
+            }
+
+            var chunkSize = suggestedResolution.PixelSize();
+            var multiplier = suggestedResolution.Multiplier();
+            RectI scaledLatestSize = (RectI)(new RectD(VecI.Zero, LatestSize * multiplier)).RoundOutwards();
+
+            RectI? preciseBounds = null;
+
+            var possibleChunks = new HashSet<VecI>();
+            foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
+                possibleChunks.Add(pos);
+
+            foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
+                possibleChunks.Add(pos);
+
+            foreach (var chunkPos in possibleChunks)
+            {
+                var committedChunk = GetCommittedChunk(chunkPos, suggestedResolution);
+                var latestChunk = GetLatestChunk(chunkPos, suggestedResolution);
+
+                Chunk? chunk;
+                bool isTempChunk = false;
+
+                if (latestChunk != null && committedChunk != null)
+                {
+                    // both exist, need to merge
+                    var tempChunk = Chunk.Create(ProcessingColorSpace, suggestedResolution);
+                    tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
+                        ReplacingPaint);
+                    blendModePaint.BlendMode = blendMode;
+                    tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.Surface.DrawingSurface, 0, 0,
+                        blendModePaint);
+                    if (lockTransparency)
+                        OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface,
+                            committedChunk.Surface.DrawingSurface);
+                    chunk = tempChunk;
+                    isTempChunk = true;
+                }
+                else if (latestChunk != null)
+                {
+                    chunk = latestChunk;
+                }
+                else
+                {
+                    chunk = committedChunk;
+                }
+
+
+                if (chunk != null)
+                {
+                    RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
+                        .Intersect(scaledLatestSize).Translate(-chunkPos * chunkSize);
+
+                    RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
+                    if (chunkPreciseBounds is null)
+                        continue;
+                    RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
+
+                    preciseBounds ??= globalChunkBounds;
+                    preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
+
+                    if (isTempChunk)
+                    {
+                        chunk.Dispose();
+                    }
+                }
+                else
+                {
+                    if (fallbackToChunkAligned)
+                    {
+                        return FindChunkAlignedMostUpToDateBounds();
+                    }
+
+                    RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
+                        .Intersect(new RectI(VecI.Zero, LatestSize)).Translate(-chunkPos * FullChunkSize);
+
+                    RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
+                    if (chunkPreciseBounds is null)
+                        continue;
+                    RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
+                        .Offset(chunkPos * chunkSize).RoundOutwards();
+
+                    preciseBounds ??= globalChunkBounds;
+                    preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
+                }
+            }
+
+            preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
+            preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, LatestSize));
+
+            cachedPreciseLatestBounds = preciseBounds.GetValueOrDefault();
+            lastLatestBoundsCacheHash = GetCacheHash();
 
             return preciseBounds;
         }
@@ -368,7 +483,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// True if the chunk existed and was drawn, otherwise false
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos,
+    public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
         Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
         lock (lockObject)
@@ -426,7 +541,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
-    public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface,
+    public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface,
         VecD pos,
         Paint? paint = null, SamplingOptions? sampling = null)
     {
@@ -522,7 +637,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos,
+    public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos,
         Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
         lock (lockObject)
@@ -807,6 +922,34 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
+    /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
+        BlendMode blendMode, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blendMode, style, antiAliasing,
+                customBounds);
+            EnqueueOperation(operation);
+        }
+    }
+
+    /// <param name="customBounds">Bounds used for affected chunks, will be computed from path in O(n) if null is passed</param>
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPath(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap strokeCap,
+        Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PathOperation operation = new(path, paintable, strokeWidth, strokeCap, blender, style, antiAliasing,
+                customBounds);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
     {
@@ -862,17 +1005,6 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
     }
 
-    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
-    {
-        lock (lockObject)
-        {
-            ThrowIfDisposed();
-            PixelOperation operation = new(pos, pixelProcessor, GetCommittedPixel, blendMode);
-            EnqueueOperation(operation);
-        }
-    }
-
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {
@@ -1197,6 +1329,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             var dict = new Dictionary<VecI, Surface>();
             foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
             {
@@ -1205,6 +1338,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                     var surf = new Surface(chunk.Surface.ImageInfo);
                     surf.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0);
                     dict[pos] = surf;
+                    surf.DrawingSurface.Flush();
                 }
             }
 
@@ -1319,7 +1453,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         {
             if (mask.CommittedChunkExists(chunkPos))
             {
-                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero,
+                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface.Canvas, VecI.Zero,
                     ClippingPaint);
             }
             else
@@ -1368,14 +1502,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             var clip = combinedRasterClips.AsT2;
 
             using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
-            targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
+            targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ReplacingPaint);
 
             CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
 
-            clip.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
-            clip.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
+            clip.DrawChunkOn(tempChunk.Surface.DrawingSurface.Canvas, VecI.Zero, ClippingPaint);
+            clip.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, InverseClippingPaint);
 
-            tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
+            tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, AddingPaint);
             return false;
         }
 
@@ -1496,7 +1630,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             newChunk.Surface.DrawingSurface.Canvas.Save();
             newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
 
-            newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0,
+            using var snapshot = existingFullResChunk.Surface.DrawingSurface.Snapshot();
+            newChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, SamplingOptions.Bilinear,
                 SmoothReplacingPaint);
             newChunk.Surface.DrawingSurface.Canvas.Restore();
             committedChunks[resolution][chunkPos] = newChunk;
@@ -1608,10 +1743,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
     public int GetCacheHash()
     {
-        return commitCounter + queuedOperations.Count + operationCounter + activeClips.Count
-               + (int)blendMode + (lockTransparency ? 1 : 0)
-               + (horizontalSymmetryAxis is not null ? (int)(horizontalSymmetryAxis * 100) : 0)
-               + (verticalSymmetryAxis is not null ? (int)(verticalSymmetryAxis * 100) : 0)
-               + (clippingPath is not null ? 1 : 0);
+        HashCode hash = new HashCode();
+        hash.Add(commitCounter);
+        hash.Add(queuedOperations.Count);
+        hash.Add(operationCounter);
+
+        foreach (var queuedOperation in queuedOperations)
+        {
+            hash.Add(queuedOperation.affectedArea.GlobalArea?.GetHashCode() ?? 0);
+            hash.Add(queuedOperation.operation.GetHashCode());
+        }
+
+        hash.Add(activeClips.Count);
+        hash.Add((int)blendMode);
+        hash.Add(lockTransparency);
+        if (horizontalSymmetryAxis is not null)
+            hash.Add((int)(horizontalSymmetryAxis * 100));
+        if (verticalSymmetryAxis is not null)
+            hash.Add((int)(verticalSymmetryAxis * 100));
+        if (clippingPath is not null)
+            hash.Add(1);
+        return hash.ToHashCode();
     }
 }

+ 17 - 17
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -21,7 +21,7 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawMostUpToDateRegionOn
-    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
+    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, Canvas surface,
         VecD pos, Paint? paint = null, SamplingOptions? sampling = null, bool drawPaintOnEmpty = false)
     {
         DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn, paint, sampling, drawPaintOnEmpty);
@@ -38,7 +38,7 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawMostUpToDateRegionOnWithAffected
-    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
+    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, Canvas surface,
         AffectedArea affectedArea, VecD pos, Paint? paint = null, SamplingOptions? sampling = null, bool drawPaintOnEmpty = false)
     {
         DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn,
@@ -56,7 +56,7 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawCommittedRegionOn
-    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
+    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, Canvas surface,
         VecI pos, Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
     {
         DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawCommittedChunkOn, paint, samplingOptions, drawPaintOnEmpty);
@@ -65,13 +65,13 @@ public static class IReadOnlyChunkyImageEx
     private static void DrawRegionOn(
         RectI fullResRegion,
         ChunkResolution resolution,
-        DrawingSurface surface,
+        Canvas surface,
         VecD pos,
-        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
+        Func<VecI, ChunkResolution, Canvas, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
         Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
     {
-        int count = surface.Canvas.Save();
-        surface.Canvas.ClipRect(new RectD(pos, fullResRegion.Size));
+        int count = surface.Save();
+        surface.ClipRect(new RectD(pos, fullResRegion.Size));
 
         VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
         VecI chunkBotRight = OperationHelper.GetChunkPos(fullResRegion.BottomRight, ChunkyImage.FullChunkSize);
@@ -87,28 +87,28 @@ public static class IReadOnlyChunkyImageEx
                         offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
                         samplingOptions) && paint != null && drawPaintOnEmpty)
                 {
-                    surface.Canvas.DrawRect(new RectD(
+                    surface.DrawRect(new RectD(
                         offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
                         new VecD(resolution.PixelSize())), paint);
                 }
             }
         }
 
-        surface.Canvas.RestoreToCount(count);
+        surface.RestoreToCount(count);
     }
 
     private static void DrawRegionOn(
         RectI fullResRegion,
         ChunkResolution resolution,
-        DrawingSurface surface,
+        Canvas surface,
         VecD pos,
-        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
-        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> quickDrawingFunc,
+        Func<VecI, ChunkResolution, Canvas, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
+        Func<VecI, ChunkResolution, Canvas, VecD, Paint?, SamplingOptions?, bool> quickDrawingFunc,
         AffectedArea area,
         Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
     {
-        int count = surface.Canvas.Save();
-        surface.Canvas.ClipRect(new RectD(pos, fullResRegion.Size));
+        int count = surface.Save();
+        surface.ClipRect(new RectD(pos, fullResRegion.Size));
 
         VecI chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
         VecI chunkBotRight = OperationHelper.GetChunkPos(fullResRegion.BottomRight, ChunkyImage.FullChunkSize);
@@ -126,7 +126,7 @@ public static class IReadOnlyChunkyImageEx
                             offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
                             samplingOptions) && paint != null && drawPaintOnEmpty)
                     {
-                        surface.Canvas.DrawRect(new RectD(
+                        surface.DrawRect(new RectD(
                             offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
                             new VecD(resolution.PixelSize())), paint);
                     }
@@ -137,7 +137,7 @@ public static class IReadOnlyChunkyImageEx
                             offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
                             samplingOptions) && paint != null && drawPaintOnEmpty)
                     {
-                        surface.Canvas.DrawRect(new RectD(
+                        surface.DrawRect(new RectD(
                             offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
                             new VecD(resolution.PixelSize())), paint);
                     }
@@ -145,6 +145,6 @@ public static class IReadOnlyChunkyImageEx
             }
         }
 
-        surface.Canvas.RestoreToCount(count);
+        surface.RestoreToCount(count);
     }
 }

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

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

+ 3 - 3
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -10,9 +10,9 @@ namespace ChunkyImageLib;
 
 public interface IReadOnlyChunkyImage
 {
-    bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
-    bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
-    bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
+    bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
+    bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
+    bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface, VecD pos, Paint? paint = null, SamplingOptions? sampling = null);
     RectI? FindChunkAlignedMostUpToDateBounds();
     RectI? FindChunkAlignedCommittedBounds();
     RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full, bool fallbackToChunkAligned = false);

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

@@ -25,7 +25,7 @@ internal class ApplyMaskOperation : IDrawOperation
     
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        mask.DrawCommittedChunkOn(chunkPos, targetChunk.Resolution, targetChunk.Surface.DrawingSurface, VecI.Zero, clippingPaint);
+        mask.DrawCommittedChunkOn(chunkPos, targetChunk.Resolution, targetChunk.Surface.DrawingSurface.Canvas, VecI.Zero, clippingPaint);
     }
 
     public void Dispose()

+ 4 - 4
src/ChunkyImageLib/Operations/BresenhamLineHelper.cs

@@ -26,7 +26,7 @@ public static class BresenhamLineHelper
 
         if (x1 == x2 && y1 == y2)
         {
-            output[index] = new VecF(start);
+            output[index] = start;
             return;
         }
 
@@ -55,7 +55,7 @@ public static class BresenhamLineHelper
             dy = y1 - y2;
         }
 
-        output[index] = new VecF(x, y);
+        output[index] = new VecI(x, y);
         index++;
 
         if (dx > dy)
@@ -78,7 +78,7 @@ public static class BresenhamLineHelper
                     x += xi;
                 }
 
-                output[index] = new VecF(x, y);
+                output[index] = new VecI(x, y);
                 index++;
             }
         }
@@ -102,7 +102,7 @@ public static class BresenhamLineHelper
                     y += yi;
                 }
 
-                output[index] = new VecF(x, y);
+                output[index] = new VecI(x, y);
                 index++;
             }
         }

+ 5 - 5
src/ChunkyImageLib/Operations/ChunkyImageOperation.cs

@@ -63,12 +63,12 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
         VecI bottomLeft = OperationHelper.GetChunkPos(
             new VecI(chunkCenterOnImage.X - halfChunk.X, chunkCenterOnImage.Y + halfChunk.Y), ChunkyImage.FullChunkSize);
 
-        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawMethod = drawUpToDate ? imageToDraw.DrawMostUpToDateChunkOn : imageToDraw.DrawCommittedChunkOn;
+        Func<VecI, ChunkResolution, Canvas, VecD, Paint?, SamplingOptions?, bool> drawMethod = drawUpToDate ? imageToDraw.DrawMostUpToDateChunkOn : imageToDraw.DrawCommittedChunkOn;
         
         drawMethod(
             topLeft,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((topLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()), null, null);
 
         VecI gridShift = targetPos % ChunkyImage.FullChunkSize;
@@ -77,7 +77,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             topRight,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((topRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
         }
@@ -86,7 +86,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             bottomLeft,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((bottomLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
         }
@@ -95,7 +95,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             bottomRight,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((bottomRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
         }

+ 9 - 3
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -84,7 +84,13 @@ internal class ImageOperation : IMirroredDrawOperation
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        //customPaint.FilterQuality = chunk.Resolution != ChunkResolution.Full;
+        //customPaint.FilterQuality = targetChunk.Resolution != ChunkResolution.Full ? FilterQuality.High : FilterQuality.None;
+        var sampling = samplingOptions;
+        if (samplingOptions == SamplingOptions.Default && targetChunk.Resolution != ChunkResolution.Full)
+        {
+            sampling = SamplingOptions.Bilinear;
+        }
+
         float scaleMult = (float)targetChunk.Resolution.Multiplier();
         VecD trans = -chunkPos * ChunkPool.FullChunkSize;
 
@@ -104,12 +110,12 @@ internal class ImageOperation : IMirroredDrawOperation
             ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
             RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
 
-            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint, samplingOptions);
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint, sampling);
         }
         else
         {
             // Slower, but works with perspective transformation
-            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, samplingOptions, customPaint);
+            targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, 0, 0, sampling, customPaint);
         }
 
         targetChunk.Surface.DrawingSurface.Canvas.Restore();

+ 24 - 0
src/ChunkyImageLib/Operations/LineHelper.cs

@@ -0,0 +1,24 @@
+using Drawie.Numerics;
+
+namespace ChunkyImageLib.Operations;
+
+public static class LineHelper
+{
+    public static VecD[] GetInterpolatedPoints(VecD start, VecD end)
+    {
+        VecD delta = end - start;
+        double longest = Math.Max(Math.Abs(delta.X), Math.Abs(delta.Y));
+
+        // ensure at least 2 points and cap excessive lengths
+        int count = Math.Clamp((int)Math.Ceiling(longest) + 1, 2, 100000);
+
+        VecD[] output = new VecD[count];
+        for (int i = 0; i < count; i++)
+        {
+            double t = (double)i / (count - 1);
+            output[i] = start + delta * t;
+        }
+
+        return output;
+    }
+}

+ 53 - 1
src/ChunkyImageLib/Operations/PathOperation.cs

@@ -1,6 +1,8 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
@@ -14,6 +16,8 @@ internal class PathOperation : IMirroredDrawOperation
     private readonly Paint paint;
     private readonly RectI bounds;
 
+    private bool antiAliasing;
+
     public bool IgnoreEmptyChunks => false;
 
     public PathOperation(VectorPath path, Color color, float strokeWidth, StrokeCap cap, BlendMode blendMode, RectI? customBounds = null)
@@ -25,9 +29,39 @@ internal class PathOperation : IMirroredDrawOperation
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
     }
 
+    public PathOperation(VectorPath path, Color color, float strokeWidth, StrokeCap cap, Blender blender, RectI? customBounds = null)
+    {
+        this.path = new VectorPath(path);
+        paint = new() { Color = color, Style = PaintStyle.Stroke, StrokeWidth = strokeWidth, StrokeCap = cap, Blender = blender };
+
+        RectI floatBounds = customBounds ?? (RectI)(path.TightBounds).RoundOutwards();
+        bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
+    }
+
+    public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, BlendMode blendMode,
+        PaintStyle style, bool antiAliasing, RectI? customBounds = null)
+    {
+        this.antiAliasing = antiAliasing;
+        this.path = new VectorPath(path);
+        paint = new() { Paintable = paintable, Style = style, StrokeWidth = strokeWidth, StrokeCap = cap, BlendMode = blendMode };
+
+        RectI floatBounds = customBounds ?? (RectI)(path.Bounds).RoundOutwards();
+        bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
+    }
+
+    public PathOperation(VectorPath path, Paintable paintable, float strokeWidth, StrokeCap cap, Blender blender, PaintStyle style, bool antiAliasing, RectI? customBounds)
+    {
+        this.antiAliasing = antiAliasing;
+        this.path = new VectorPath(path);
+        paint = new() { Paintable = paintable, Style = style, StrokeWidth = strokeWidth, StrokeCap = cap, Blender = blender };
+
+        RectI floatBounds = customBounds ?? (RectI)(path.Bounds).RoundOutwards();
+        bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
+    }
+
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
-        paint.IsAntiAliased = targetChunk.Resolution != ChunkResolution.Full;
+        paint.IsAntiAliased = antiAliasing || targetChunk.Resolution != ChunkResolution.Full;
         var surf = targetChunk.Surface.DrawingSurface;
         surf.Canvas.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
@@ -52,6 +86,24 @@ internal class PathOperation : IMirroredDrawOperation
             newBounds = (RectI)newBounds.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
             newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
+        if (paint.Paintable != null)
+        {
+            if( paint.Blender != null)
+            {
+                return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.Blender, paint.Style, antiAliasing, newBounds);
+            }
+            else
+            {
+                return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode,
+                    paint.Style, antiAliasing, newBounds);
+            }
+        }
+
+        if (paint.Blender != null)
+        {
+            return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.Blender, newBounds);
+        }
+
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);
     }
 

+ 3 - 24
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -7,7 +7,6 @@ using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
 
-public delegate Color PixelProcessor(Color commited, Color upToDate);
 internal class PixelOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
@@ -15,9 +14,6 @@ internal class PixelOperation : IMirroredDrawOperation
     private readonly Color color;
     private readonly BlendMode blendMode;
     private readonly Paint paint;
-    private readonly Func<VecI, Color>? getCommitedPixelFunc = null;
-
-    private readonly PixelProcessor? colorProcessor = null;
 
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     {
@@ -27,15 +23,6 @@ internal class PixelOperation : IMirroredDrawOperation
         paint = new Paint() { BlendMode = blendMode };
     }
 
-    public PixelOperation(VecI pixel, PixelProcessor colorProcessor, Func<VecI, Color> getCommitedPixelFunc, BlendMode blendMode)
-    {
-        this.pixel = pixel;
-        this.colorProcessor = colorProcessor;
-        this.blendMode = blendMode;
-        this.getCommitedPixelFunc = getCommitedPixelFunc;
-        paint = new Paint() { BlendMode = blendMode };
-    }
-
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
         // a hacky way to make the lines look slightly better on non full res chunks
@@ -45,19 +32,15 @@ internal class PixelOperation : IMirroredDrawOperation
         surf.Canvas.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
-        surf.Canvas.DrawPoint(pixel, paint);
+
+        // Drawing points with GPU chunks doesn't work well, that's why we draw rects instead
+        surf.Canvas.DrawRect(new RectD(pixel, new VecD(1)), paint);
         surf.Canvas.Restore();
     }
 
     private Color GetColor(Chunk chunk, VecI chunkPos)
     {
         Color pixelColor = color;
-        if (colorProcessor != null && getCommitedPixelFunc != null)
-        {
-            var pos = pixel - chunkPos * ChunkyImage.FullChunkSize;
-            pixelColor = colorProcessor(getCommitedPixelFunc(pixel), chunk.Surface.GetSrgbPixel(pos));
-        }
-
         return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));
     }
 
@@ -73,10 +56,6 @@ internal class PixelOperation : IMirroredDrawOperation
             pixelRect = (RectI)pixelRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
             pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY);
-        if (colorProcessor != null && getCommitedPixelFunc != null)
-        {
-            return new PixelOperation(pixelRect.Pos, colorProcessor, getCommitedPixelFunc, blendMode);
-        }
 
         return new PixelOperation(pixelRect.Pos, color, blendMode);
     }

+ 6 - 1
src/ChunkyImageLib/Operations/PixelsOperation.cs

@@ -34,7 +34,12 @@ internal class PixelsOperation : IMirroredDrawOperation
         surf.Canvas.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
-        surf.Canvas.DrawPoints(PointMode.Points, pixels, paint);
+        //surf.Canvas.DrawPoints(PointMode.Points, pixels, paint);
+        // Drawing points with GPU chunks doesn't work well, that's why we draw rects instead
+        foreach (var pixel in pixels)
+        {
+            surf.Canvas.DrawRect(new RectD((VecD)pixel, new VecD(1)), paint);
+        }
         surf.Canvas.Restore();
     }
 

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

@@ -193,7 +193,7 @@ internal class RectangleOperation : IMirroredDrawOperation
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
         if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 ||
-            (!Data.Stroke.AnythingVisible && !Data.FillPaintable.AnythingVisible))
+            (Data.Stroke is not { AnythingVisible: true } && Data.FillPaintable is not { AnythingVisible: true }))
             return new();
 
         RectI affRect = (RectI)new ShapeCorners(Data.Center, Data.Size).AsRotated(Data.Angle, Data.Center).AABBBounds

+ 3 - 1
src/ChunkyImageLib/Operations/TextureOperation.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
@@ -89,7 +90,8 @@ internal class TextureOperation : IMirroredDrawOperation
 
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
-        targetChunk.Surface.DrawingSurface.Canvas.DrawImage(toPaint.DrawingSurface.Snapshot(), 0, 0, customPaint);
+        using var snap = toPaint.DrawingSurface.Snapshot();
+        targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snap, 0, 0, customPaint);
         targetChunk.Surface.DrawingSurface.Canvas.Restore();
     }
 

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 61055feed27354e6be969055fc0ee5db3c7d3b94
+Subproject commit 1de89b352c0fdeb9487a98b9093828fb295a9df2

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.6</AvaloniaVersion>
+		    <AvaloniaVersion>11.3.9-cibuild0004033-alpha</AvaloniaVersion>
     </PropertyGroup>
   
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 1604a0bb1fdf1d0016bfc82752c85b3266bed2c2
+Subproject commit 078670ac485e8cb92ec84bd88d112713219e3256

+ 1 - 1
src/PixiEditor.Api.CGlueMSBuild/PixiEditor.Api.CGlueMSBuild.csproj

@@ -14,7 +14,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.6" ExcludeAssets="Runtime" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.14.28" ExcludeAssets="Runtime" />
     <PackageReference Include="Mono.Cecil" Version="0.11.6">
       <PrivateAssets>All</PrivateAssets>
       <IncludeAssets>All</IncludeAssets>

+ 6 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableExposed_ChangeInfo.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph.Blackboard;
+
+public record BlackboardVariableExposed_ChangeInfo(string VariableName, bool Value) : IChangeInfo
+{
+
+}

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableRemoved_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph.Blackboard;
+
+public record BlackboardVariableRemoved_ChangeInfo(string VariableName) : IChangeInfo;

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariable_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph.Blackboard;
+
+public record BlackboardVariable_ChangeInfo(string Name, Type Type, object Value, double Min, double Max, string? Unit) : IChangeInfo;

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/RenameBlackboardVariable_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph.Blackboard;
+
+public record RenameBlackboardVariable_ChangeInfo(string OldName, string NewName) : IChangeInfo;

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

@@ -27,7 +27,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
     {
     }
 
-    internal static CreateFolder_ChangeInfo FromFolder(FolderNode folder)
+    public static CreateFolder_ChangeInfo FromFolder(FolderNode folder)
     {
         return new CreateFolder_ChangeInfo(
             folder.GetNodeTypeUniqueName(),

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

@@ -33,7 +33,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 
     public bool LockTransparency { get; }
 
-    internal static CreateLayer_ChangeInfo FromLayer(LayerNode layer)
+    public static CreateLayer_ChangeInfo FromLayer(LayerNode layer)
     {
         return new CreateLayer_ChangeInfo(
             layer.GetNodeTypeUniqueName(),

+ 4 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/NestedDocumentLink_ChangeInfo.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+
+public record struct NestedDocumentLink_ChangeInfo(Guid NodeId, string? OriginalFilePath, Guid ReferenceId) : IChangeInfo;
+

+ 17 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -92,6 +92,23 @@ internal class AnimationData : IReadOnlyAnimationData
         return TryFindKeyFrameCallback(id, out keyFrame, null);
     }
 
+    public IReadOnlyAnimationData Clone()
+    {
+        AnimationData clone = new AnimationData(document)
+        {
+            FrameRate = FrameRate,
+            OnionFrames = OnionFrames,
+            DefaultEndFrame = DefaultEndFrame,
+            OnionOpacity = OnionOpacity
+        };
+        foreach (var keyFrame in keyFrames)
+        {
+            clone.keyFrames.Add(keyFrame.Clone());
+        }
+
+        return clone;
+    }
+
     private bool TryFindKeyFrameCallback<T>(Guid id, out T? foundKeyFrame,
         Action<KeyFrame, GroupKeyFrame?> onFound = null) where T : IReadOnlyKeyFrame
     {

+ 19 - 0
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushData.cs

@@ -0,0 +1,19 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
+
+public struct BrushData
+{
+    public IReadOnlyNodeGraph BrushGraph { get; set; }
+    public bool AntiAliasing { get; set; }
+    public float StrokeWidth { get; set; }
+    public Guid TargetBrushNodeId { get; set; }
+    public bool ForcePressure { get; set; }
+
+    public BrushData(IReadOnlyNodeGraph brushGraph, Guid targetBrushNodeId)
+    {
+        BrushGraph = brushGraph;
+        TargetBrushNodeId = targetBrushNodeId;
+    }
+}

+ 614 - 0
src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs

@@ -0,0 +1,614 @@
+using System.Diagnostics;
+using ChunkyImageLib.Operations;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
+using DrawingApiBlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
+
+public class BrushEngine : IDisposable
+{
+    private TextureCache cache = new();
+    private VecD lastPos;
+    private VecD startPos;
+    private double lastPressure = 1.0;
+    private int lastAppliedPointIndex = -1;
+    private int lastAppliedHistoryIndex = -1;
+    private VecI lastCachedTexturePaintableSize = VecI.Zero;
+    private TexturePaintable? lastCachedTexturePaintable = null;
+    private readonly List<RecordedPoint> pointsHistory = new();
+
+    private bool drawnOnce = false;
+    
+    // Configuration: How many previous points to average.
+    // Higher = smoother but more "laggy" pressure response.
+    // 10 points is roughly 10 pixels of stroke history.
+    public int PressureSmoothingWindowSize { get; set; } = 10;
+
+    public void ResetState()
+    {
+        lastAppliedPointIndex = -1;
+        lastAppliedHistoryIndex = -1;
+        lastPos = VecD.Zero;
+        lastPressure = 1.0;
+        startPos = VecD.Zero;
+        drawnOnce = false;
+        pointsHistory.Clear();
+    }
+
+    /// <summary>
+    /// Calculates a smoothed pressure value based on the previous points in history.
+    /// This acts as a low-pass filter to remove jitter.
+    /// </summary>
+    private float GetSmoothedPressure(double targetPressure)
+    {
+        if (pointsHistory.Count == 0)
+            return (float)targetPressure;
+
+        double sum = 0;
+        int count = 0;
+
+        // Iterate backwards through history
+        for (int i = pointsHistory.Count - 1; i >= 0 && count < PressureSmoothingWindowSize; i--)
+        {
+            sum += pointsHistory[i].PointerInfo.Pressure;
+            count++;
+        }
+
+        // Add the current target to the average so we pull towards the new value
+        sum += targetPressure;
+        count++;
+
+        return (float)(sum / count);
+    }
+
+    public void ExecuteBrush(ChunkyImage target, BrushData brushData, List<RecordedPoint> points,
+        KeyFrameTime frameTime,
+        ColorSpace cs, SamplingOptions samplingOptions)
+    {
+        if (brushData.BrushGraph == null)
+        {
+            return;
+        }
+
+        if (brushData.BrushGraph.LookupNode(brushData.TargetBrushNodeId) is not BrushOutputNode brushNode)
+        {
+            return;
+        }
+        
+        for (int i = lastAppliedPointIndex + 1; i < points.Count; i++)
+        {
+            RecordedPoint previousPoint = points[i];
+            if (i == 0)
+            {
+                if (pointsHistory.Count > 0)
+                {
+                    previousPoint = pointsHistory[^1];
+                }
+            }
+            else
+            {
+                previousPoint = points[i - 1];
+            }
+            
+            var currentPoint = points[i];
+            var dist = VecD.Distance(previousPoint.Position, currentPoint.Position);
+
+            if (dist > 0.5)
+            {
+                var interpolated = LineHelper.GetInterpolatedPoints(previousPoint.Position,
+                    currentPoint.Position);
+                
+                for (int j = 1; j < interpolated.Length; j++)
+                {
+                    var pt = interpolated[j];
+                    
+                    double ratio = VecD.Distance(previousPoint.Position, pt) /
+                                   VecD.Distance(previousPoint.Position, currentPoint.Position);
+                                   
+                    double linearTargetPressure = previousPoint.PointerInfo.Pressure +
+                                                  (currentPoint.PointerInfo.Pressure - previousPoint.PointerInfo.Pressure) * ratio;
+
+                    float smoothedPressure = GetSmoothedPressure(linearTargetPressure);
+                    
+                    pointsHistory.Add(new RecordedPoint(pt,
+                        currentPoint.PointerInfo with { Pressure = smoothedPressure },
+                        currentPoint.KeyboardInfo,
+                        currentPoint.EditorData));
+                }
+            }
+            else
+            {
+                float smoothedPressure = GetSmoothedPressure(currentPoint.PointerInfo.Pressure);
+                
+                pointsHistory.Add(new RecordedPoint(currentPoint.Position,
+                    currentPoint.PointerInfo with { Pressure = smoothedPressure },
+                    currentPoint.KeyboardInfo,
+                    currentPoint.EditorData));
+            }
+        }
+        
+        lastAppliedPointIndex = points.Count - 1;
+
+        float strokeWidth = brushData.StrokeWidth;
+        float spacing = brushNode.Spacing.Value / 100f;
+        int startingIndex = Math.Max(lastAppliedHistoryIndex, 0);
+        float spacingPressure = pointsHistory.Count < startingIndex + 1
+            ? (float)lastPressure
+            : pointsHistory[startingIndex].PointerInfo.Pressure;
+
+        for (int i = Math.Max(lastAppliedHistoryIndex, 0); i < pointsHistory.Count; i++)
+        {
+            var point = pointsHistory[i];
+
+            float spacingPixels = (strokeWidth * spacingPressure) * spacing;
+            if (VecD.Distance(lastPos, point.Position) < spacingPixels)
+                continue;
+
+            ExecuteVectorShapeBrush(target, brushNode, brushData, point.Position, frameTime, cs, samplingOptions,
+                point.PointerInfo,
+                point.KeyboardInfo,
+                point.EditorData);
+
+            spacingPressure = brushNode.Pressure.Value;
+
+            lastPos = point.Position;
+        }
+
+        lastPressure = pointsHistory.Count > 0 ? pointsHistory[^1].PointerInfo.Pressure : 1.0;
+        lastAppliedHistoryIndex = pointsHistory.Count - 1;
+    }
+
+
+    public void ExecuteBrush(ChunkyImage? target, BrushData brushData, VecD point, KeyFrameTime frameTime,
+        ColorSpace cs,
+        SamplingOptions samplingOptions, PointerInfo pointerInfo, KeyboardInfo keyboardInfo, EditorData editorData)
+    {
+        var brushNode = brushData.BrushGraph?.LookupNode(brushData.TargetBrushNodeId) as BrushOutputNode;
+        if (brushNode == null)
+        {
+            return;
+        }
+
+        ExecuteVectorShapeBrush(target, brushNode, brushData, point, frameTime, cs, samplingOptions, pointerInfo,
+            keyboardInfo,
+            editorData);
+    }
+
+    private void ExecuteVectorShapeBrush(ChunkyImage? target, BrushOutputNode brushNode, BrushData brushData,
+        VecD point,
+        KeyFrameTime frameTime,
+        ColorSpace colorSpace, SamplingOptions samplingOptions,
+        PointerInfo pointerInfo, KeyboardInfo keyboardInfo, EditorData editorData)
+    {
+        bool shouldErase = editorData.PrimaryColor.A == 0;
+
+        var imageBlendMode = shouldErase ? DrawingApiBlendMode.DstOut : brushNode.ImageBlendMode.Value;
+
+        if (!drawnOnce)
+        {
+            startPos = point;
+            lastPos = point;
+            drawnOnce = true;
+            target?.SetBlendMode(imageBlendMode);
+        }
+
+        float strokeWidth = brushData.StrokeWidth;
+        var rect = new RectD(point - new VecD((strokeWidth / 2f)), new VecD(strokeWidth));
+        if (brushNode.SnapToPixels.Value)
+        {
+            VecI vecIpoint = (VecI)point;
+            rect = (RectD)new RectI(vecIpoint - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
+        }
+
+        bool requiresSampleTexture = GraphUsesSampleTexture(brushData.BrushGraph, brushNode);
+        bool requiresFullTexture = GraphUsesFullTexture(brushData.BrushGraph, brushNode);
+        Texture? surfaceUnderRect = null;
+        Texture? fullTexture = null;
+        Texture texture = null;
+
+        if (brushNode.AlwaysClear.Value)
+        {
+            target?.EnqueueClear();
+        }
+
+        if (requiresSampleTexture && rect.Width > 0 && rect.Height > 0 && target != null)
+        {
+            surfaceUnderRect = UpdateSurfaceUnderRect(target, (RectI)rect.RoundOutwards(), colorSpace,
+                brushNode.AllowSampleStacking.Value);
+        }
+
+        if (requiresFullTexture && target != null)
+        {
+            fullTexture = UpdateFullTexture(target, colorSpace, brushNode.AllowSampleStacking.Value);
+        }
+
+        BrushRenderContext context = new BrushRenderContext(
+            texture?.DrawingSurface.Canvas, frameTime, ChunkResolution.Full,
+            brushNode.FitToStrokeSize.NonOverridenValue
+                ? ((RectI)rect.RoundOutwards()).Size
+                : target?.CommittedSize ?? VecI.Zero,
+            target?.CommittedSize ?? VecI.Zero,
+            colorSpace, samplingOptions, brushData,
+            surfaceUnderRect, fullTexture, brushData.BrushGraph,
+            startPos, lastPos)
+        {
+            PointerInfo = pointerInfo,
+            EditorData = shouldErase
+                ? new EditorData(editorData.PrimaryColor.WithAlpha(255), editorData.SecondaryColor)
+                : editorData,
+            KeyboardInfo = keyboardInfo
+        };
+
+        // Evaluate shape without painting if no target
+        if (target == null)
+        {
+            brushData.BrushGraph.Execute(brushNode, context);
+            if (brushNode.VectorShape.Value == null)
+                return;
+
+            using var shape = brushNode.VectorShape.Value.ToPath(true);
+            return;
+        }
+
+        if (requiresSampleTexture && brushNode.VectorShape.Value != null)
+        {
+            brushData.BrushGraph.Execute(brushNode, context);
+
+            using var shape = brushNode.VectorShape.Value.ToPath(true);
+            EvaluateShape(brushNode.AutoPosition.Value, shape, brushNode.VectorShape.Value, rect,
+                brushNode.SnapToPixels.Value, brushNode.FitToStrokeSize.Value, brushNode.Pressure.Value);
+
+            if (shape.Bounds is { Width: > 0, Height: > 0 })
+            {
+                context.TargetSampledTexture?.Dispose();
+                surfaceUnderRect = UpdateSurfaceUnderRect(target, (RectI)shape.TightBounds.RoundOutwards(), colorSpace,
+                    brushNode.AllowSampleStacking.Value);
+                context.TargetSampledTexture = surfaceUnderRect;
+                context.RenderOutputSize = ((RectI)shape.TightBounds.RoundOutwards()).Size;
+            }
+        }
+
+        var previous = brushNode.Previous.Value;
+        while (previous != null)
+        {
+            var data = new BrushData(previous, brushData.TargetBrushNodeId)
+            {
+                AntiAliasing = brushData.AntiAliasing,
+                StrokeWidth = brushData.StrokeWidth,
+                ForcePressure = brushData.ForcePressure
+            };
+
+            var previousBrushNode = previous.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode;
+            PaintBrush(target, data, point, previousBrushNode, context, rect);
+            previous = previousBrushNode?.Previous.Value;
+        }
+
+        PaintBrush(target, brushData, point, brushNode, context, rect);
+    }
+
+    private void PaintBrush(ChunkyImage target, BrushData brushData, VecD point, BrushOutputNode brushNode,
+        BrushRenderContext context, RectD rect)
+    {
+        brushData.BrushGraph.Execute(brushNode, context);
+
+        var vectorShape = brushNode.VectorShape.Value;
+        if (vectorShape == null)
+        {
+            return;
+        }
+
+        bool autoPosition = brushNode.AutoPosition.Value;
+        bool fitToStrokeSize = brushNode.FitToStrokeSize.Value;
+        float pressure = brushData.ForcePressure && brushNode.Pressure.Connection == null
+            ? context.PointerInfo.Pressure
+            : brushNode.Pressure.Value;
+        var content = brushNode.Content.Value;
+        var contentTexture = brushNode.ContentTexture;
+        bool antiAliasing = brushData.AntiAliasing;
+        var fill = brushNode.Fill.Value;
+        var stroke = brushNode.Stroke.Value;
+        bool snapToPixels = brushNode.SnapToPixels.Value;
+        bool canReuseStamps = brushNode.CanReuseStamps.Value;
+        Blender? stampBlender = brushNode.UseCustomStampBlender.Value ? brushNode.LastStampBlender : null;
+        //Blender? imageBlender = brushNode.UseCustomImageBlender.Value ? brushNode.LastImageBlender : null;
+
+        if (PaintBrush(target, autoPosition, vectorShape, rect, fitToStrokeSize, pressure, content, contentTexture,
+                stampBlender, brushNode.StampBlendMode.Value, antiAliasing, fill, stroke, snapToPixels, canReuseStamps))
+        {
+            lastPos = point;
+        }
+    }
+
+    public bool PaintBrush(ChunkyImage target, bool autoPosition, ShapeVectorData vectorShape,
+        RectD rect, bool fitToStrokeSize, float pressure, Painter? content,
+        Texture? contentTexture, Blender? blender, DrawingApiBlendMode blendMode, bool antiAliasing, Paintable fill, Paintable stroke,
+        bool snapToPixels, bool canReuseStamps)
+    {
+        var path = vectorShape.ToPath(true);
+        if (path == null)
+        {
+            return false;
+        }
+
+        EvaluateShape(autoPosition, path, vectorShape, rect, snapToPixels, fitToStrokeSize, pressure);
+
+        StrokeCap strokeCap = StrokeCap.Butt;
+        PaintStyle strokeStyle = PaintStyle.Fill;
+
+        var paintable = fill;
+
+        if (fill != null && fill.AnythingVisible)
+        {
+            strokeStyle = PaintStyle.Fill;
+        }
+        else
+        {
+            strokeStyle = PaintStyle.Stroke;
+            paintable = stroke;
+        }
+
+        if (vectorShape is PathVectorData pathData)
+        {
+            strokeCap = pathData.StrokeLineCap;
+        }
+
+        if (paintable is { AnythingVisible: true })
+        {
+            if (blender != null)
+            {
+                target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
+                    strokeCap, blender, strokeStyle, antiAliasing, null);
+            }
+            else
+            {
+                target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
+                    strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            }
+        }
+
+        if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
+        {
+            strokeStyle = PaintStyle.Stroke;
+            if (blender != null)
+            {
+                target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
+                    strokeCap, blender, strokeStyle, antiAliasing, null);
+            }
+            else
+            {
+                target.EnqueueDrawPath(path, stroke, vectorShape.StrokeWidth,
+                    strokeCap, blendMode, strokeStyle, antiAliasing, null);
+            }
+        }
+
+        if (content != null)
+        {
+            if (contentTexture != null)
+            {
+                TexturePaintable brushPaintable;
+                if (canReuseStamps)
+                {
+                    if (lastCachedTexturePaintableSize != contentTexture.Size || lastCachedTexturePaintable == null)
+                    {
+                        lastCachedTexturePaintable?.Dispose();
+                        lastCachedTexturePaintable = new TexturePaintable(new Texture(contentTexture), false);
+                        lastCachedTexturePaintableSize = contentTexture.Size;
+                    }
+
+                    brushPaintable = lastCachedTexturePaintable;
+                }
+                else
+                {
+                    brushPaintable = new TexturePaintable(new Texture(contentTexture), true);
+                }
+
+                if (blender != null)
+                {
+                    target.EnqueueDrawPath(path, brushPaintable, vectorShape.StrokeWidth,
+                        StrokeCap.Butt, blender, PaintStyle.Fill, antiAliasing, null);
+                }
+                else
+                {
+                    target.EnqueueDrawPath(path, brushPaintable, vectorShape.StrokeWidth,
+                        StrokeCap.Butt, blendMode, PaintStyle.Fill, antiAliasing, null);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private Texture UpdateFullTexture(ChunkyImage target, ColorSpace colorSpace, bool sampleLatest)
+    {
+        var texture = cache.RequestTexture(1, target.LatestSize, colorSpace);
+        if (!sampleLatest)
+        {
+            target.DrawCommittedRegionOn(new RectI(VecI.Zero, target.LatestSize), ChunkResolution.Full,
+                texture.DrawingSurface.Canvas, VecI.Zero);
+            return texture;
+        }
+
+        target.DrawMostUpToDateRegionOn(new RectI(VecI.Zero, target.LatestSize), ChunkResolution.Full,
+            texture.DrawingSurface.Canvas, VecI.Zero);
+        return texture;
+    }
+
+    private Texture UpdateSurfaceUnderRect(ChunkyImage target, RectI rect, ColorSpace colorSpace, bool sampleLatest)
+    {
+        var surfaceUnderRect = cache.RequestTexture(0, rect.Size, colorSpace);
+
+        if (sampleLatest)
+        {
+            target.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface.Canvas,
+                VecI.Zero);
+        }
+        else
+        {
+            target.DrawCommittedRegionOn(rect, ChunkResolution.Full, surfaceUnderRect.DrawingSurface.Canvas, VecI.Zero);
+        }
+
+        return surfaceUnderRect;
+    }
+
+    private bool GraphUsesSampleTexture(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode)
+    {
+        return GraphUsesInput(graph, brushNode, node => node.TargetSampleTexture.Connections);
+    }
+
+    private bool GraphUsesFullTexture(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode)
+    {
+        return GraphUsesInput(graph, brushNode, node => node.TargetFullTexture.Connections);
+    }
+
+    private bool GraphUsesInput(IReadOnlyNodeGraph graph, IReadOnlyNode brushNode,
+        Func<IBrushSampleTextureNode, IReadOnlyCollection<IInputProperty>> getConnections)
+    {
+        var sampleTextureNodes = graph.AllNodes.Where(x => x is IBrushSampleTextureNode).ToList();
+        if (sampleTextureNodes.Count == 0)
+        {
+            return false;
+        }
+
+        foreach (var node in sampleTextureNodes)
+        {
+            if (node is IBrushSampleTextureNode brushSampleTextureNode)
+            {
+                var connections = getConnections(brushSampleTextureNode);
+                if (connections.Count == 0)
+                {
+                    continue;
+                }
+
+                foreach (var connection in connections)
+                {
+                    bool found = false;
+                    connection.Connection.Node.TraverseForwards(x =>
+                    {
+                        if (x == brushNode)
+                        {
+                            found = true;
+                            return false;
+                        }
+
+                        return true;
+                    });
+
+                    if (found)
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public VectorPath? EvaluateShape(VecD point, BrushData brushData)
+    {
+        return EvaluateShape(point, brushData,
+            brushData.BrushGraph.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode);
+    }
+
+    public VectorPath? EvaluateShape(VecD point, BrushData brushData, BrushOutputNode brushNode)
+    {
+        var vectorShape = brushNode.VectorShape.Value;
+        if (vectorShape == null)
+        {
+            return null;
+        }
+
+        float strokeWidth = brushData.StrokeWidth;
+        var rect = new RectD(point - new VecD((strokeWidth / 2f)), new VecD(strokeWidth));
+
+        bool autoPosition = brushNode.AutoPosition.Value;
+        bool fitToStrokeSize = brushNode.FitToStrokeSize.Value;
+        float pressure = brushNode.Pressure.Value;
+        bool snapToPixels = brushNode.SnapToPixels.Value;
+
+        if (snapToPixels)
+        {
+            rect = (RectD)(new RectI((VecI)point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth)));
+        }
+
+        var path = vectorShape.ToPath(true);
+        if (path == null)
+        {
+            return null;
+        }
+
+        EvaluateShape(autoPosition, path, vectorShape, rect, snapToPixels, fitToStrokeSize, pressure);
+
+        return path;
+    }
+
+    private static void EvaluateShape(bool autoPosition, VectorPath path, ShapeVectorData vectorShape, RectD rect,
+        bool snapToPixels, bool fitToStrokeSize, float pressure)
+    {
+        if (fitToStrokeSize)
+        {
+            VecD scale = new VecD(rect.Size.X / (float)path.TightBounds.Width,
+                rect.Size.Y / (float)path.TightBounds.Height);
+            if (scale.IsNaNOrInfinity())
+            {
+                scale = VecD.Zero;
+            }
+
+            VecD uniformScale = new VecD(Math.Min(scale.X, scale.Y));
+            VecD center = autoPosition ? rect.Center : vectorShape.TransformedAABB.Center;
+
+            path.Transform(Matrix3X3.CreateScale((float)uniformScale.X, (float)uniformScale.Y, (float)center.X,
+                (float)center.Y));
+
+            if (snapToPixels)
+            {
+                // stretch to pixels
+                path.Transform(Matrix3X3.CreateScale(
+                    (float)(Math.Round(path.TightBounds.Width) / path.TightBounds.Width),
+                    (float)(Math.Round(path.TightBounds.Height) / path.TightBounds.Height),
+                    (float)center.X,
+                    (float)center.Y));
+            }
+        }
+
+        if (autoPosition)
+        {
+            path.Offset(vectorShape.TransformedAABB.Pos - vectorShape.GeometryAABB.Pos);
+            path.Offset(rect.Center - path.TightBounds.Center);
+
+            if (snapToPixels)
+            {
+                path.Offset(
+                    new VecD(Math.Round(path.TightBounds.Pos.X) - path.TightBounds.Pos.X,
+                        Math.Round(path.TightBounds.Pos.Y) - path.TightBounds.Pos.Y));
+            }
+        }
+
+
+        Matrix3X3 pressureScale = Matrix3X3.CreateScale(pressure, pressure, (float)rect.Center.X,
+            (float)rect.Center.Y);
+        path.Transform(pressureScale);
+    }
+
+    public void Dispose()
+    {
+        cache.Dispose();
+        lastCachedTexturePaintable?.Dispose();
+    }
+}

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changeables/Brushes/IBrush.cs

@@ -0,0 +1,10 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
+
+public interface IBrush
+{
+    public string? FilePath { get; }
+    public Guid OutputNodeId { get; }
+    IReadOnlyDocument Document { get; }
+}

+ 20 - 0
src/PixiEditor.ChangeableDocument/Changeables/Brushes/RecordedPoint.cs

@@ -0,0 +1,20 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Brushes;
+
+public struct RecordedPoint
+{
+    public VecD Position { get; }
+    public PointerInfo PointerInfo { get; }
+    public KeyboardInfo KeyboardInfo { get; }
+    public EditorData EditorData { get; }
+
+    public RecordedPoint(VecD position, PointerInfo pointerInfo, KeyboardInfo keyboardInfo, EditorData editorData)
+    {
+        Position = position;
+        PointerInfo = pointerInfo;
+        KeyboardInfo = keyboardInfo;
+        EditorData = editorData;
+    }
+}

+ 49 - 6
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -9,13 +9,14 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables;
 
 internal class Document : IChangeable, IReadOnlyDocument
 {
-    public Guid DocumentId { get; } = Guid.NewGuid();
+    public Guid DocumentId { get; private set; } = Guid.NewGuid();
     IReadOnlyNodeGraph IReadOnlyDocument.NodeGraph => NodeGraph;
     IReadOnlySelection IReadOnlyDocument.Selection => Selection;
     IReadOnlyAnimationData IReadOnlyDocument.AnimationData => AnimationData;
@@ -32,6 +33,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     IReadOnlyReferenceLayer? IReadOnlyDocument.ReferenceLayer => ReferenceLayer;
     public DocumentRenderer Renderer { get; }
+    public IReadOnlyBlackboard Blackboard => NodeGraph.Blackboard;
     public ColorSpace ProcessingColorSpace { get; internal set; } = ColorSpace.CreateSrgbLinear();
 
     /// <summary>
@@ -39,10 +41,10 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// </summary>
     public static VecI DefaultSize { get; } = new VecI(64, 64);
 
-    internal NodeGraph NodeGraph { get; } = new();
-    internal Selection Selection { get; } = new();
+    internal NodeGraph NodeGraph { get; private set; } = new();
+    internal Selection Selection { get; private set; } = new();
     internal ReferenceLayer? ReferenceLayer { get; set; }
-    internal AnimationData AnimationData { get; }
+    internal AnimationData AnimationData { get; private set; }
     public VecI Size { get; set; } = DefaultSize;
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
@@ -104,7 +106,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             chunkyImage.DrawCommittedRegionOn(
                 new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 ChunkResolution.Full,
-                chunkSurface.DrawingSurface,
+                chunkSurface.DrawingSurface.Canvas,
                 VecI.Zero);
 
             image = chunkSurface;
@@ -173,6 +175,42 @@ internal class Document : IChangeable, IReadOnlyDocument
         return new DocumentNodePipe<T>(this, layerId);
     }
 
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> CreateGraphPipe()
+    {
+        return new DocumentGraphPipe(this);
+    }
+
+    public IReadOnlyDocument Clone(bool preserveDocumentId = false)
+    {
+        var clone = new Document
+        {
+            Size = Size,
+            ProcessingColorSpace = ProcessingColorSpace,
+            HorizontalSymmetryAxisEnabled = HorizontalSymmetryAxisEnabled,
+            VerticalSymmetryAxisEnabled = VerticalSymmetryAxisEnabled,
+            HorizontalSymmetryAxisY = HorizontalSymmetryAxisY,
+            VerticalSymmetryAxisX = VerticalSymmetryAxisX,
+            ReferenceLayer = ReferenceLayer?.Clone(),
+            NodeGraph = NodeGraph?.Clone() as NodeGraph,
+            AnimationData = AnimationData?.Clone() as AnimationData,
+            Selection = Selection != null ? new Selection() { SelectionPath = Selection.SelectionPath != null ? new VectorPath(Selection.SelectionPath) : null } : null
+        };
+
+        if (preserveDocumentId)
+        {
+            clone.DocumentId = DocumentId;
+        }
+
+        return clone;
+    }
+
+    public IReadOnlyStructureNode[] GetStructureTreeInOrder()
+    {
+        var list = new List<IReadOnlyStructureNode>();
+        ForEveryReadonlyMember(NodeGraph, member => list.Add(member));
+        return list.ToArray();
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>
@@ -444,7 +482,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     private void ExtractLayers(FolderNode folder, List<Guid> list)
     {
-        if(folder.Content.Connection == null) return;
+        if (folder.Content.Connection == null) return;
         folder.Content.Connection.Node.TraverseBackwards(node =>
         {
             if (node is LayerNode layer && !list.Contains(layer.Id))
@@ -455,4 +493,9 @@ internal class Document : IChangeable, IReadOnlyDocument
             return true;
         });
     }
+
+    object ICloneable.Clone()
+    {
+        return Clone();
+    }
 }

+ 16 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentGraphPipe.cs

@@ -0,0 +1,16 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal class DocumentGraphPipe : DocumentMemoryPipe<IReadOnlyNodeGraph>
+{
+    public DocumentGraphPipe(Document document) : base(document)
+    {
+    }
+
+    protected override IReadOnlyNodeGraph? GetData()
+    {
+        return Document.NodeGraph;
+    }
+}

+ 22 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentReference.cs

@@ -0,0 +1,22 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+public class DocumentReference : ICloneable
+{
+    public string? OriginalFilePath { get; set; }
+    public Guid ReferenceId { get; set; }
+    public IReadOnlyDocument DocumentInstance { get; set; }
+
+    public DocumentReference(string? originalFilePath, Guid referenceId, IReadOnlyDocument documentInstance)
+    {
+        OriginalFilePath = originalFilePath;
+        ReferenceId = referenceId;
+        DocumentInstance = documentInstance;
+    }
+
+    public object Clone()
+    {
+        return new DocumentReference(OriginalFilePath, ReferenceId, DocumentInstance.Clone());
+    }
+}

+ 119 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Blackboard.cs

@@ -0,0 +1,119 @@
+using PixiEditor.Common;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public class Blackboard : IReadOnlyBlackboard
+{
+    private Dictionary<string, Variable> variables = new Dictionary<string, Variable>();
+    public IReadOnlyDictionary<string, Variable> Variables => variables;
+
+    IReadOnlyDictionary<string, IReadOnlyVariable> IReadOnlyBlackboard.Variables =>
+        variables.ToDictionary(kv => kv.Key, kv => (IReadOnlyVariable)kv.Value);
+
+    public void SetVariable(string name, Type type, object value, string? unit = null, double? min = null,
+        double? max = null, bool isExposed = true)
+    {
+        if (variables.ContainsKey(name))
+        {
+            variables[name].Name = name;
+            variables[name].Type = type;
+            variables[name].Value = value;
+            variables[name].Unit = unit;
+            variables[name].Min = min;
+            variables[name].Max = max;
+            variables[name].IsExposed = isExposed;
+        }
+        else
+        {
+            variables[name] = new Variable
+            {
+                Type = type,
+                Value = value,
+                Name = name,
+                Unit = unit,
+                Min = min,
+                Max = max,
+                IsExposed = isExposed
+            };
+        }
+    }
+
+    public Variable? GetVariable(string name)
+    {
+        if (name == null)
+            return null;
+
+        return variables.GetValueOrDefault(name);
+    }
+
+    public bool RemoveVariable(string name)
+    {
+        return variables.Remove(name);
+    }
+
+    IReadOnlyVariable IReadOnlyBlackboard.GetVariable(string variableName)
+    {
+        return GetVariable(variableName);
+    }
+
+    public void RenameVariable(string oldName, string newName)
+    {
+        if (!variables.ContainsKey(oldName) || variables.ContainsKey(newName))
+            throw new ArgumentException("Invalid variable names for renaming.");
+
+        var variable = variables[oldName];
+        variables.Remove(oldName);
+        variable.Name = newName;
+        variables[newName] = variable;
+    }
+
+    public int GetCacheHash()
+    {
+        HashCode hash = new HashCode();
+        hash.Add(variables.Count);
+        foreach (var variable in variables.Values)
+        {
+            hash.Add(variable.Name);
+            hash.Add(variable.Type);
+            if (variable.Value != null)
+            {
+                if (variable.Value is ICacheable cacheable)
+                {
+                    hash.Add(cacheable.GetCacheHash());
+                }
+                else
+                {
+                    hash.Add(variable.Value.GetHashCode());
+                }
+            }
+
+            hash.Add(variable.Unit);
+            hash.Add(variable.Min);
+            hash.Add(variable.Max);
+        }
+
+        return hash.ToHashCode();
+    }
+}
+
+public class Variable : IReadOnlyVariable
+{
+    public string Name { get; set; }
+    public Type Type { get; set; }
+    public object Value { get; set; }
+    public string? Unit { get; set; }
+    public double? Min { get; set; }
+    public double? Max { get; set; }
+    public bool IsExposed { get; set; } = true;
+}
+
+public interface IReadOnlyVariable
+{
+    public string Name { get; }
+    public Type Type { get; }
+    public object Value { get; }
+    public string? Unit { get; }
+    public double? Min { get; }
+    public double? Max { get; }
+    public bool IsExposed { get; }
+}

+ 47 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/BrushRenderContext.cs

@@ -0,0 +1,47 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+
+public class BrushRenderContext : RenderContext
+{
+    public BrushData BrushData { get; }
+    public Texture TargetSampledTexture { get; set; }
+    public Texture TargetFullTexture { get; }
+    public VecD StartPoint { get; }
+    public VecD LastAppliedPoint { get; }
+
+    public BrushRenderContext(Canvas renderSurface, KeyFrameTime frameTime, ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, ColorSpace processingColorSpace, SamplingOptions desiredSampling, BrushData brushData, Texture? targetSampledTexture, Texture? targetFullTexture, IReadOnlyNodeGraph graph, VecD startPoint, VecD lastAppliedPoint, double opacity = 1) : base(renderSurface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, desiredSampling, graph, opacity)
+    {
+        BrushData = brushData;
+        StartPoint = startPoint;
+        LastAppliedPoint = lastAppliedPoint;
+        TargetSampledTexture = targetSampledTexture;
+        TargetFullTexture = targetFullTexture;
+    }
+
+    public override RenderContext Clone()
+    {
+        return new BrushRenderContext(RenderSurface, FrameTime, ChunkResolution, RenderOutputSize, DocumentSize,
+            ProcessingColorSpace, DesiredSamplingOptions, BrushData, TargetSampledTexture, TargetFullTexture, Graph,
+            StartPoint, LastAppliedPoint, Opacity)
+        {
+            VisibleDocumentRegion = VisibleDocumentRegion,
+            AffectedArea = AffectedArea,
+            FullRerender = FullRerender,
+            TargetOutput = TargetOutput,
+            PreviewTextures = PreviewTextures,
+            EditorData = EditorData,
+            KeyboardInfo = KeyboardInfo,
+            PointerInfo = PointerInfo,
+            ViewportData = ViewportData,
+            CloneDepth = CloneDepth + 1,
+        };
+    }
+}

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

@@ -92,13 +92,28 @@ public class FuncContext
         if (!HasContext && first is Int1 firstInt && second is Int1 secondInt)
         {
             Int2 constantInt = new Int2("");
-            constantInt.ConstantValue = new VecI(firstInt.ConstantValue, secondInt.ConstantValue);
+            constantInt.ConstantValue = new VecI(GetIntConstant(firstInt), GetIntConstant(secondInt));
             return constantInt;
         }
 
         return Builder.ConstructInt2(first, second);
     }
 
+    private static int GetIntConstant(Int1 int1)
+    {
+        object constant = int1.GetConstant();
+        return constant switch
+        {
+            int i => i,
+            double d => (int)d,
+            float f => (int)f,
+            long l => (int)l,
+            byte b => b,
+            short s => s,
+            _ => 0
+        };
+    }
+
     public Half4 NewHalf4(Expression r, Expression g, Expression b, Expression a)
     {
         if (!HasContext && r is Float1 firstFloat && g is Float1 secondFloat && b is Float1 thirdFloat &&
@@ -198,8 +213,14 @@ public class FuncContext
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
+                float value = 0;
+                if (getFrom?.Value != null)
+                {
+                    value = (float)getFrom.Value(this).ConstantValue;
+                }
+
                 string uniformName = $"float_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(uniformName, (float)getFrom.Value(this).ConstantValue);
+                Builder.AddUniform(uniformName, value);
                 return new Float1(uniformName);
             }
 
@@ -225,8 +246,14 @@ public class FuncContext
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
+                int value = 0;
+                if (getFrom?.Value != null)
+                {
+                    value = getFrom.Value(this)?.ConstantValue ?? 0;
+                }
+
                 string uniformName = $"int_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(uniformName, (int)getFrom.Value(this).ConstantValue);
+                Builder.AddUniform(uniformName, value);
                 return new Int1(uniformName);
             }
 
@@ -256,9 +283,9 @@ public class FuncContext
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
-                Half4 color = getFrom.Value(this);
+                Half4 color = getFrom?.Value != null ? getFrom.Value(this) : new Half4("");
                 color.VariableName = $"color_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(color.VariableName, color.ConstantValue);
+                Builder.AddUniform(color.VariableName, color?.ConstantValue ?? Colors.Transparent);
                 return color;
             }
 
@@ -283,9 +310,9 @@ public class FuncContext
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
-                Float2 value = getFrom.Value(this);
+                Float2 value = getFrom?.Value != null ? getFrom.Value(this) : new Float2("");
                 value.VariableName = $"float2_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? VecD.Zero);
                 return value;
             }
 
@@ -310,9 +337,9 @@ public class FuncContext
         {
             if (getFrom?.Connection == null || !IsFuncType(getFrom))
             {
-                Int2 value = getFrom.Value(this);
+                Int2 value = getFrom?.Value != null ? getFrom.Value(this) : new Int2("");
                 value.VariableName = $"int2_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? VecI.Zero);
                 return value;
             }
 
@@ -337,9 +364,9 @@ public class FuncContext
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
-                Float3x3 value = getFrom.Value(this);
+                Float3x3 value = getFrom?.Value != null ? getFrom.Value(this) : new Float3x3("");
                 value.VariableName = $"float3x3_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? Matrix3X3.Identity);
                 return value;
             }
 
@@ -382,7 +409,7 @@ public class FuncContext
         if (!HasContext && matrixExpression is Float3x3 float3x3)
         {
             Float3x3 constantMatrix = new Float3x3("");
-            constantMatrix.ConstantValue = float3x3.ConstantValue;
+            constantMatrix.ConstantValue = float3x3?.ConstantValue ?? Matrix3X3.Identity;
             return constantMatrix;
         }
 

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/IReadOnlyBlackboard.cs

@@ -0,0 +1,10 @@
+using System.Collections;
+using PixiEditor.Common;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public interface IReadOnlyBlackboard : ICacheable
+{
+    public IReadOnlyVariable? GetVariable(string variableName);
+    public IReadOnlyDictionary<string, IReadOnlyVariable> Variables { get; }
+}

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

@@ -30,7 +30,7 @@ public class InputProperty : IInputProperty
                 return _internalValue;
             }
 
-            var connectionValue = Connection.Value;
+            var connectionValue = Connection?.Value;
 
             if (connectionValue is null)
             {
@@ -265,6 +265,7 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             if (value is ShaderExpressionVariable shaderExpression)
             {
                 value = shaderExpression.GetConstant();
+                if (value is null) return default(T);
             }
 
             if (!ConversionTable.TryConvert(value, ValueType, out object result))

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

@@ -4,5 +4,5 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IClipSource
 {
-    public void DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto);
+    public void DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto);
 }

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

@@ -44,6 +44,6 @@ public interface IReadOnlyNode : ICacheable
 
     public IInputProperty? GetInputProperty(string internalName);
     public IOutputProperty? GetOutputProperty(string internalName);
-    public void SerializeAdditionalData(Dictionary<string, object> additionalData);
+    public void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData);
     public string GetNodeTypeUniqueName();
 }

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

@@ -1,17 +1,25 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.Common;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 public interface IReadOnlyNodeGraph : ICacheable, IDisposable
 {
+    public IReadOnlyBlackboard Blackboard { get; }
     public IReadOnlyCollection<IReadOnlyNode> AllNodes { get; }
+    public IReadOnlyNode LookupNode(Guid guid);
     public IReadOnlyNode OutputNode { get; }
     public void AddNode(IReadOnlyNode node);
     public void RemoveNode(IReadOnlyNode node);
     public bool TryTraverse(Action<IReadOnlyNode> action);
+    public bool TryTraverse(IReadOnlyNode end, Action<IReadOnlyNode> action);
     public void Execute(RenderContext context);
+    public void Execute(IReadOnlyNode end, RenderContext context);
     Queue<IReadOnlyNode> CalculateExecutionQueue(IReadOnlyNode endNode);
     public IReadOnlyNodeGraph Clone();
+    public event Action<NodeOutputsChanged_ChangeInfo> NodeOutputsChanged;
+    public void Execute(IEnumerable<IReadOnlyNode> nodes, RenderContext context);
 }

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

@@ -20,5 +20,5 @@ public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject
     public RectD? GetTightBounds(KeyFrameTime frameTime);
     public ChunkyImage? EmbeddedMask { get; }
     public ShapeCorners GetTransformationCorners(KeyFrameTime frameTime);
-    public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output);
+    public void RenderForOutput(RenderContext context, Canvas renderTarget, RenderOutputProperty output);
 }

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs

@@ -12,8 +12,8 @@ public class SceneObjectRenderContext : RenderContext
     public bool RenderSurfaceIsScene { get; }
     public RenderOutputProperty TargetPropertyOutput { get; }
 
-    public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, DrawingSurface surface, RectD localBounds, KeyFrameTime frameTime,
-        ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, SamplingOptions desiredSampling, double opacity) : base(surface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, desiredSampling, opacity)
+    public SceneObjectRenderContext(RenderOutputProperty targetPropertyOutput, Canvas surface, RectD localBounds, KeyFrameTime frameTime,
+        ChunkResolution chunkResolution, VecI renderOutputSize, VecI documentSize, bool renderSurfaceIsScene, ColorSpace processingColorSpace, SamplingOptions desiredSampling, IReadOnlyNodeGraph graph, double opacity) : base(surface, frameTime, chunkResolution, renderOutputSize, documentSize, processingColorSpace, desiredSampling, graph, opacity)
     {
         TargetPropertyOutput = targetPropertyOutput;
         LocalBounds = localBounds;
@@ -22,13 +22,18 @@ public class SceneObjectRenderContext : RenderContext
 
     public override RenderContext Clone()
     {
-        return new SceneObjectRenderContext(TargetPropertyOutput, RenderSurface, LocalBounds, FrameTime, ChunkResolution, RenderOutputSize, DocumentSize, RenderSurfaceIsScene, ProcessingColorSpace, DesiredSamplingOptions, Opacity)
+        return new SceneObjectRenderContext(TargetPropertyOutput, RenderSurface, LocalBounds, FrameTime, ChunkResolution, RenderOutputSize, DocumentSize, RenderSurfaceIsScene, ProcessingColorSpace, DesiredSamplingOptions, Graph, Opacity)
         {
             VisibleDocumentRegion = VisibleDocumentRegion,
             AffectedArea = AffectedArea,
             FullRerender = FullRerender,
             TargetOutput = TargetOutput,
             PreviewTextures = PreviewTextures,
+            EditorData = EditorData,
+            KeyboardInfo = KeyboardInfo,
+            PointerInfo = PointerInfo,
+            ViewportData = ViewportData,
+            CloneDepth = CloneDepth + 1,
         };
     }
 }

+ 94 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -2,25 +2,38 @@
 using System.Diagnostics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 public class NodeGraph : IReadOnlyNodeGraph
 {
-    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
+    private Dictionary<IReadOnlyNode, ImmutableList<IReadOnlyNode>?> cachedExecutionList;
 
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public IReadOnlyDictionary<Guid, Node> NodeLookup => nodeLookup;
+
     public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
     public Node? CustomOutputNode { get; set; }
 
+    public Blackboard Blackboard { get; } = new();
+
     private Dictionary<Guid, Node> nodeLookup = new();
 
+    public event Action<NodeOutputsChanged_ChangeInfo>? NodeOutputsChanged;
+
     IReadOnlyCollection<IReadOnlyNode> IReadOnlyNodeGraph.AllNodes => Nodes;
     IReadOnlyNode IReadOnlyNodeGraph.OutputNode => OutputNode;
+    IReadOnlyBlackboard IReadOnlyNodeGraph.Blackboard => Blackboard;
 
+    bool isExecuting = false;
+
+    public IReadOnlyNode LookupNode(Guid guid)
+    {
+        return nodeLookup[guid];
+    }
 
     public void AddNode(Node node)
     {
@@ -31,6 +44,7 @@ public class NodeGraph : IReadOnlyNodeGraph
 
         node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
+        node.OutputsChanged += () => NodeOutputsChanged?.Invoke(NodeOutputsChanged_ChangeInfo.FromNode(node));
         nodeLookup[node.Id] = node;
         ResetCache();
     }
@@ -106,12 +120,37 @@ public class NodeGraph : IReadOnlyNodeGraph
             newGraph.CustomOutputNode = mappedOutputNode;
         }
 
+        // Clone blackboard variables
+        foreach (var kvp in Blackboard.Variables)
+        {
+            object valueCopy;
+            if (kvp.Value.Value is ICloneable cloneable)
+            {
+                valueCopy = cloneable.Clone();
+            }
+            else
+            {
+                valueCopy = kvp.Value.Value;
+            }
+
+            newGraph.Blackboard.SetVariable(kvp.Key, kvp.Value.Type, valueCopy, kvp.Value.Unit, kvp.Value.Min, kvp.Value.Max, kvp.Value.IsExposed);
+        }
+
         return newGraph;
     }
 
     private ImmutableList<IReadOnlyNode> CalculateExecutionQueueInternal(IReadOnlyNode outputNode)
     {
-        return cachedExecutionList ??= GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
+        var cached = this.cachedExecutionList?.GetValueOrDefault(outputNode);
+        if (cached != null)
+        {
+            return cached;
+        }
+
+        var calculated = GraphUtils.CalculateExecutionQueue(outputNode).ToImmutableList();
+        cachedExecutionList ??= new Dictionary<IReadOnlyNode, ImmutableList<IReadOnlyNode>?>();
+        cachedExecutionList[outputNode] = calculated;
+        return calculated;
     }
 
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -128,9 +167,14 @@ public class NodeGraph : IReadOnlyNodeGraph
 
     public bool TryTraverse(Action<IReadOnlyNode> action)
     {
-        if (OutputNode == null) return false;
+        return TryTraverse(OutputNode, action);
+    }
+
+    public bool TryTraverse(IReadOnlyNode end, Action<IReadOnlyNode> action)
+    {
+        if (end == null) return false;
 
-        var queue = CalculateExecutionQueueInternal(OutputNode);
+        var queue = CalculateExecutionQueueInternal(end);
 
         foreach (var node in queue)
         {
@@ -140,16 +184,53 @@ public class NodeGraph : IReadOnlyNodeGraph
         return true;
     }
 
-    bool isexecuting = false;
 
     public void Execute(RenderContext context)
     {
-        if (isexecuting) return;
-        isexecuting = true;
-        if (OutputNode == null) return;
+        Execute(OutputNode, context);
+    }
+
+    public void Execute(IEnumerable<IReadOnlyNode> nodes, RenderContext context)
+    {
+        isExecuting = true;
         if (!CanExecute()) return;
 
-        var queue = CalculateExecutionQueueInternal(OutputNode);
+        HashSet<IReadOnlyNode> executedNodes = new();
+        foreach (var exposeVariableNode in nodes)
+        {
+            var queue = CalculateExecutionQueueInternal(exposeVariableNode);
+
+            foreach (var node in queue)
+            {
+                if (!executedNodes.Add(node)) continue;
+
+                lock (node)
+                {
+                    if (node is Node typedNode)
+                    {
+                        if (typedNode.IsDisposed) continue;
+
+                        typedNode.ExecuteInternal(context);
+                    }
+                    else
+                    {
+                        node.Execute(context);
+                    }
+                }
+            }
+        }
+
+        isExecuting = false;
+    }
+
+    public void Execute(IReadOnlyNode end, RenderContext context)
+    {
+        //if (isExecuting) return;
+        isExecuting = true;
+        if (end == null) return;
+        if (!CanExecute()) return;
+
+        var queue = CalculateExecutionQueueInternal(end);
 
         foreach (var node in queue)
         {
@@ -168,7 +249,7 @@ public class NodeGraph : IReadOnlyNodeGraph
             }
         }
 
-        isexecuting = false;
+        isExecuting = false;
     }
 
     private bool CanExecute()
@@ -192,7 +273,9 @@ public class NodeGraph : IReadOnlyNodeGraph
     public int GetCacheHash()
     {
         HashCode hash = new();
-        foreach (var node in Nodes)
+        var queue = CalculateExecutionQueueInternal(OutputNode);
+
+        foreach (var node in queue)
         {
             int nodeCache = node.GetCacheHash();
             hash.Add(nodeCache);

+ 5 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
@@ -8,18 +9,21 @@ public class TimeNode : Node
 {
     public OutputProperty<int> ActiveFrame { get; set; }
     public OutputProperty<double> NormalizedTime { get; set; }
+    public OutputProperty<double> Time { get; set; }
 
 
     public TimeNode()
     {
         ActiveFrame = CreateOutput("ActiveFrame", "ACTIVE_FRAME", 0);
         NormalizedTime = CreateOutput("NormalizedTime", "NORMALIZED_TIME", 0.0);
+        Time = CreateOutput("Time", "TIME", 0.0);
     }
     
     protected override void OnExecute(RenderContext context)
     {
         ActiveFrame.Value = context.FrameTime.Frame;
         NormalizedTime.Value = context.FrameTime.NormalizedTime;
+        Time.Value = (DateTime.UtcNow - Process.GetCurrentProcess().StartTime.ToUniversalTime()).TotalSeconds;
     }
 
     public override Node CreateCopy()

+ 54 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/BlackboardVariableValueNode.cs

@@ -0,0 +1,54 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("BlackboardVariableValue")]
+public class BlackboardVariableValueNode : Node
+{
+    public const string NameProperty = "VariableName";
+    public InputProperty<string> VariableName { get; }
+    public OutputProperty<object> Value { get; }
+
+    public BlackboardVariableValueNode()
+    {
+        VariableName = CreateInput(NameProperty, "VARIABLE_NAME", string.Empty);
+        Value = CreateOutput<object>("Value", "VALUE", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var variable = context.Graph?.Blackboard.GetVariable(VariableName.Value);
+        Value.Value = variable?.Value;
+    }
+
+    public void UpdateValuesFromBlackboard(IReadOnlyBlackboard nodeGraphBlackboard)
+    {
+        var variable = nodeGraphBlackboard.GetVariable(VariableName.Value);
+        if (variable != null)
+        {
+            Value.Value = variable.Value;
+            NotifyOutputs();
+        }
+        else
+        {
+            Value.Value = null;
+        }
+    }
+
+    private void NotifyOutputs()
+    {
+        foreach (var prop in Value.Connections)
+        {
+            if (prop.Node is IInputDependentOutputs dependentOutputsNode)
+            {
+                dependentOutputsNode.UpdateOutputs();
+            }
+        }
+    }
+
+    public override Node CreateCopy()
+    {
+        return new BlackboardVariableValueNode();
+    }
+}

+ 357 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs

@@ -0,0 +1,357 @@
+using System.Collections;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
+using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+
+[NodeInfo(NodeId)]
+public class BrushOutputNode : Node
+{
+    public const string NodeId = "BrushOutput";
+    public const string BrushNameProperty = "BrushName";
+    public const string FitToStrokeSizeProperty = "FitToStrokeSize";
+    public const int PointPreviewSize = 50;
+    public const int StrokePreviewSizeX = 200;
+    public const int StrokePreviewSizeY = 50;
+
+    public const string DefaultBlenderCode = @"
+    vec4 main(vec4 src, vec4 dst) {
+    	return src + (1 - src.a) * dst;
+    }
+";
+
+    private string? lastStampBlenderCode = "";
+    private string? lastImageBlenderCode = "";
+
+    public Blender? LastStampBlender => cachedStampBlender;
+    public Blender? LastImageBlender => cachedImageBlender;
+
+    private Blender? cachedStampBlender = null;
+    private Blender? cachedImageBlender = null;
+
+    public InputProperty<string> BrushName { get; }
+    public InputProperty<ShapeVectorData> VectorShape { get; }
+    public InputProperty<Paintable> Stroke { get; }
+    public InputProperty<Paintable> Fill { get; }
+    public RenderInputProperty Content { get; }
+    public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> StampBlendMode { get; }
+    public InputProperty<Drawie.Backend.Core.Surfaces.BlendMode> ImageBlendMode { get; }
+    public InputProperty<bool> UseCustomStampBlender { get; }
+    public InputProperty<string> CustomStampBlenderCode { get; }
+    public InputProperty<Matrix3X3> Transform { get; }
+    public InputProperty<float> Pressure { get; }
+    public InputProperty<float> Spacing { get; }
+    public InputProperty<bool> FitToStrokeSize { get; }
+    public InputProperty<bool> AutoPosition { get; }
+    public InputProperty<bool> AllowSampleStacking { get; }
+    public InputProperty<bool> AlwaysClear { get; }
+    public InputProperty<bool> SnapToPixels { get; }
+
+    public InputProperty<string> Tags { get; }
+
+    // Indicate whether stamps from this brush can be reused when drawing with the same brush again. Optimization option.
+    public InputProperty<bool> CanReuseStamps { get; }
+
+    public InputProperty<IReadOnlyNodeGraph> Previous { get; }
+
+    internal Texture ContentTexture;
+
+    private TextureCache cache = new();
+
+    private ChunkyImage? previewChunkyImage;
+    private BrushEngine previewEngine = new BrushEngine() { PressureSmoothingWindowSize = 0 };
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    public Guid PersistentId { get; private set; } = Guid.NewGuid();
+
+    public const string PreviewSvg =
+        "M0.25 99.4606C0.25 99.4606 60.5709 79.3294 101.717 99.4606C147.825 122.019 199.75 99.4606 199.75 99.4606";
+
+    public const int YOffsetInPreview = -88;
+    public const string UseCustomStampBlenderProperty = "UseCustomStampBlender";
+    public const string CustomStampBlenderCodeProperty = "CustomStampBlender";
+    public const string StampBlendModeProperty = "StampBlendMode";
+
+    private VectorPath? previewVectorPath;
+
+    public BrushOutputNode()
+    {
+        BrushName = CreateInput<string>(BrushNameProperty, "NAME", "Unnamed");
+        VectorShape = CreateInput<ShapeVectorData>("VectorShape", "SHAPE", null);
+        Stroke = CreateInput<Paintable>("Stroke", "STROKE", null);
+        Fill = CreateInput<Paintable>("Fill", "FILL", null);
+        Content = CreateRenderInput("Content", "CONTENT");
+        Transform = CreateInput<Matrix3X3>("Transform", "TRANSFORM", Matrix3X3.Identity);
+        ImageBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>("BlendMode", "BLEND_MODE",
+            Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
+        StampBlendMode = CreateInput<Drawie.Backend.Core.Surfaces.BlendMode>(StampBlendModeProperty, "STAMP_BLEND_MODE",
+            Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
+
+        UseCustomStampBlender = CreateInput<bool>(UseCustomStampBlenderProperty, "USE_CUSTOM_STAMP_BLENDER", false);
+
+        CustomStampBlenderCode =
+            CreateInput<string>(CustomStampBlenderCodeProperty, "CUSTOM_STAMP_BLENDER_CODE", DefaultBlenderCode)
+                .WithRules(validator => validator.Custom(ValidateBlenderCode));
+        CanReuseStamps = CreateInput<bool>("CanReuseStamps", "CAN_REUSE_STAMPS", false);
+
+        Pressure = CreateInput<float>("Pressure", "PRESSURE", 1f);
+        Spacing = CreateInput<float>("Spacing", "SPACING", 0);
+        FitToStrokeSize = CreateInput<bool>(FitToStrokeSizeProperty, "FIT_TO_STROKE_SIZE", true);
+        AutoPosition = CreateInput<bool>("AutoPosition", "AUTO_POSITION", true);
+        AllowSampleStacking = CreateInput<bool>("AllowSampleStacking", "ALLOW_SAMPLE_STACKING", false);
+        AlwaysClear = CreateInput<bool>("AlwaysClear", "ALWAYS_CLEAR", false);
+        SnapToPixels = CreateInput<bool>("SnapToPixels", "SNAP_TO_PIXELS", false);
+        Tags = CreateInput<string>("Tags", "TAGS", "");
+        Previous = CreateInput<IReadOnlyNodeGraph>("Previous", "PREVIOUS", null);
+    }
+
+    private ValidatorResult ValidateBlenderCode(object? value)
+    {
+        if (value is string code)
+        {
+            Blender? blender = Blender.CreateFromString(code, out string? error);
+            if (blender != null)
+            {
+                blender.Dispose();
+                return new ValidatorResult(true, null);
+            }
+
+            return new ValidatorResult(false, error);
+        }
+
+        return new ValidatorResult(false, "Blender code must be a string.");
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (Content.Value != null)
+        {
+            if (context.RenderOutputSize.LongestAxis > 0)
+            {
+                ContentTexture = cache.RequestTexture(0, context.RenderOutputSize, context.ProcessingColorSpace);
+                ContentTexture.DrawingSurface.Canvas.Save();
+                ContentTexture.DrawingSurface.Canvas.SetMatrix(Transform.Value);
+                Content.Value.Paint(context, ContentTexture.DrawingSurface.Canvas);
+                ContentTexture.DrawingSurface.Canvas.Restore();
+            }
+        }
+
+        if (UseCustomStampBlender.Value)
+        {
+            if (CustomStampBlenderCode.Value != lastStampBlenderCode || cachedStampBlender == null)
+            {
+                cachedStampBlender?.Dispose();
+                cachedStampBlender = Blender.CreateFromString(CustomStampBlenderCode.Value, out _);
+                lastStampBlenderCode = CustomStampBlenderCode.Value;
+            }
+        }
+        else
+        {
+            cachedStampBlender?.Dispose();
+            cachedStampBlender = null;
+            lastStampBlenderCode = "";
+        }
+
+        RenderPreviews(context.GetPreviewTexturesForNode(Id), context);
+    }
+
+    public override void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
+    {
+        base.SerializeAdditionalData(target, additionalData);
+        additionalData["PersistentId"] = PersistentId;
+    }
+
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data,
+        List<IChangeInfo> infos)
+    {
+        base.DeserializeAdditionalData(target, data, infos);
+        if (data.TryGetValue("PersistentId", out var persistentIdObj))
+        {
+            if (persistentIdObj is Guid persistentId)
+            {
+                PersistentId = persistentId;
+            }
+            else if (persistentIdObj is string persistentIdStr && Guid.TryParse(persistentIdStr, out Guid parsedGuid))
+            {
+                PersistentId = parsedGuid;
+            }
+        }
+    }
+
+    private void RenderPreviews(List<PreviewRenderRequest>? previews, RenderContext ctx)
+    {
+        var previewToRender = previews;
+        if (previewToRender == null || previewToRender.Count == 0)
+            return;
+
+        foreach (var preview in previewToRender)
+        {
+            if (preview.Texture == null)
+                continue;
+
+            int saved = preview.Texture.DrawingSurface.Canvas.Save();
+            preview.Texture.DrawingSurface.Canvas.Clear();
+
+            var bounds = new RectD(0, 0, 200, 200);
+
+            RenderContext adjusted =
+                PreviewUtility.CreatePreviewContext(ctx, new VecD(1), bounds.Size, preview.Texture.Size);
+
+            adjusted.RenderSurface = preview.Texture.DrawingSurface.Canvas;
+            RenderPreview(preview.Texture.DrawingSurface, adjusted);
+            preview.Texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
+    }
+
+    private void RenderPreview(DrawingSurface surface, RenderContext context)
+    {
+        if (previewChunkyImage == null)
+        {
+            previewChunkyImage = new ChunkyImage(new VecI(200, 200), context.ProcessingColorSpace);
+        }
+
+        RectI rect;
+
+        previewChunkyImage.EnqueueClear();
+        previewChunkyImage.CommitChanges();
+
+        int maxSize = 50;
+        float offset = 0;
+
+        int[] sizes = new int[] { 10, 25, 50 };
+        const int spacing = 10;
+        const int marginEdges = 30;
+        VecD pos = VecD.Zero;
+        previewEngine.ResetState();
+
+        for (var i = 0; i < sizes.Length; i++)
+        {
+            var size = sizes[i];
+            int x = marginEdges + (int)(i * (size + spacing + (maxSize - size) / 2f));
+            pos = new VecI(x, maxSize);
+
+            previewEngine.ExecuteBrush(previewChunkyImage,
+                new BrushData(context.Graph, Id) { StrokeWidth = size, AntiAliasing = true },
+                (VecI)pos, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
+                new PointerInfo(pos, 1, 0, VecD.Zero, new VecD(0, 1)),
+                new KeyboardInfo(),
+                new EditorData(Colors.White, Colors.Black));
+        }
+        previewChunkyImage.CommitChanges();
+
+        DrawStrokePreview(previewChunkyImage, context, maxSize);
+
+        previewChunkyImage.CommitChanges();
+        previewChunkyImage.DrawCommittedChunkOn(
+            VecI.Zero, ChunkResolution.Full, surface.Canvas, VecD.Zero);
+    }
+
+    public void DrawStrokePreview(ChunkyImage target, RenderContext context, int maxSize, VecD shift = default)
+    {
+        if (previewVectorPath == null)
+        {
+            previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
+        }
+
+        float offset = 0;
+        float pressure;
+        VecD pos;
+        List<RecordedPoint> points = new();
+        previewEngine.ResetState();
+
+        while (offset <= target.CommittedSize.X)
+        {
+            pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
+            var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
+            pos = vec4D.XY;
+            pos = new VecD(pos.X, pos.Y + maxSize / 2f) + shift;
+
+            points.Add(new RecordedPoint((VecI)pos, new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
+
+            previewEngine.ExecuteBrush(target,
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true }, points,
+                context.FrameTime,
+                context.ProcessingColorSpace, context.DesiredSamplingOptions);
+            offset += 1;
+        }
+    }
+
+    public IEnumerable<float> DrawStrokePreviewEnumerable(ChunkyImage target, RenderContext context, int maxSize,
+        VecD shift = default)
+    {
+        if (previewVectorPath == null)
+        {
+            previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
+        }
+
+        List<RecordedPoint> points = new();
+
+        float offset = 0;
+        float pressure;
+        VecD pos;
+        previewEngine.ResetState();
+
+        while (offset <= target.CommittedSize.X)
+        {
+            pressure = (float)Math.Sin((offset / target.CommittedSize.X) * Math.PI);
+            var vec4D = previewVectorPath.GetPositionAndTangentAtDistance(offset, false);
+            pos = vec4D.XY;
+            pos = new VecD(pos.X, pos.Y + maxSize / 2f) + shift;
+            points.Add(new RecordedPoint((VecI)pos, new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(), new EditorData(Colors.White, Colors.Black)));
+
+            previewEngine.ExecuteBrush(target,
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true },
+                points, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions);
+            offset += 1;
+            yield return offset;
+        }
+    }
+
+    public void DrawPointPreview(ChunkyImage img, RenderContext context, int size, VecD pos)
+    {
+        previewEngine.ResetState();
+        previewEngine.ExecuteBrush(img,
+            new BrushData(context.Graph, Id) { StrokeWidth = size, AntiAliasing = true },
+            pos, context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
+            new PointerInfo(pos, 1, 0, VecD.Zero, new VecD(0, 1)),
+            new KeyboardInfo(),
+            new EditorData(Colors.White, Colors.Black));
+    }
+
+    public override Node CreateCopy()
+    {
+        return new BrushOutputNode();
+    }
+
+    public override void Dispose()
+    {
+        previewEngine.Dispose();
+        previewChunkyImage?.Dispose();
+        previewChunkyImage = null;
+
+        previewVectorPath?.Dispose();
+        previewVectorPath = null;
+        cache.Dispose();
+        base.Dispose();
+    }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/IBrushSampleTextureNode.cs

@@ -0,0 +1,9 @@
+using Drawie.Backend.Core;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+
+public interface IBrushSampleTextureNode
+{
+    public OutputProperty<Texture> TargetSampleTexture { get; }
+    public OutputProperty<Texture> TargetFullTexture { get; }
+}

+ 50 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/StrokeInfoNode.cs

@@ -0,0 +1,50 @@
+using Drawie.Backend.Core;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+
+[NodeInfo("StrokeInfo")]
+public class StrokeInfoNode : Node, IBrushSampleTextureNode
+{
+    public OutputProperty<float> StrokeWidth { get; }
+    public OutputProperty<VecD> StartPoint { get; }
+    public OutputProperty<VecD> LastAppliedPoint { get; }
+    public OutputProperty<Texture> TargetSampleTexture { get; }
+    public OutputProperty<Texture> TargetFullTexture { get; }
+
+    public StrokeInfoNode()
+    {
+        StrokeWidth = CreateOutput<float>("StrokeWidth", "STROKE_WIDTH", 1f);
+        StartPoint = CreateOutput<VecD>("StartPoint", "START_POINT", VecD.Zero);
+        LastAppliedPoint = CreateOutput<VecD>("LastAppliedPoint", "LAST_APPLIED_POINT", VecD.Zero);
+        TargetSampleTexture = CreateOutput<Texture>("TargetSampleTexture", "TARGET_SAMPLE_TEXTURE", null);
+        TargetFullTexture = CreateOutput<Texture>("TargetFullTexture", "TARGET_FULL_TEXTURE", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (context is not BrushRenderContext brushRenderContext)
+            return;
+
+        StrokeWidth.Value = brushRenderContext.BrushData.StrokeWidth;
+        StartPoint.Value = brushRenderContext.StartPoint;
+        LastAppliedPoint.Value = brushRenderContext.LastAppliedPoint;
+
+        if (TargetSampleTexture.Connections.Count > 0)
+        {
+            TargetSampleTexture.Value = brushRenderContext.TargetSampledTexture;
+        }
+
+        if (TargetFullTexture.Connections.Count > 0)
+        {
+            TargetFullTexture.Value = brushRenderContext.TargetFullTexture;
+        }
+    }
+
+    public override Node CreateCopy()
+    {
+        return new StrokeInfoNode();
+    }
+}

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

@@ -39,47 +39,47 @@ public class CombineChannelsNode : RenderNode
     }
 
     
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
-        int saved = surface.Canvas.SaveLayer();
+        int saved = surface.SaveLayer();
         if (Red.Value is { } red)
         {
             _screenPaint.ColorFilter = _redFilter;
             
-            int savedRed = surface.Canvas.SaveLayer(_screenPaint);
+            int savedRed = surface.SaveLayer(_screenPaint);
             red.Paint(context, surface);
             
-            surface.Canvas.RestoreToCount(savedRed);
+            surface.RestoreToCount(savedRed);
         }
 
         if (Green.Value is { } green)
         {
             _screenPaint.ColorFilter = _greenFilter;
-            int savedGreen = surface.Canvas.SaveLayer(_screenPaint);
+            int savedGreen = surface.SaveLayer(_screenPaint);
             green.Paint(context, surface);
             
-            surface.Canvas.RestoreToCount(savedGreen);
+            surface.RestoreToCount(savedGreen);
         }
 
         if (Blue.Value is { } blue)
         {
             _screenPaint.ColorFilter = _blueFilter;
-            int savedBlue = surface.Canvas.SaveLayer(_screenPaint);
+            int savedBlue = surface.SaveLayer(_screenPaint);
             blue.Paint(context, surface);
             
-            surface.Canvas.RestoreToCount(savedBlue);
+            surface.RestoreToCount(savedBlue);
         }
 
         if (Alpha.Value is { } alpha)
         {
             _clearPaint.ColorFilter = Grayscale.Value ? Filters.AlphaGrayscaleFilter : null;
-            int savedAlpha = surface.Canvas.SaveLayer(_clearPaint);
+            int savedAlpha = surface.SaveLayer(_clearPaint);
             alpha.Paint(context, surface);
             
-            surface.Canvas.RestoreToCount(savedAlpha);
+            surface.RestoreToCount(savedAlpha);
         }
             
-        surface.Canvas.RestoreToCount(saved);
+        surface.RestoreToCount(saved);
     }
 
     public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
@@ -128,7 +128,7 @@ public class CombineChannelsNode : RenderNode
 
     public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
     }
 
     public override Node CreateCopy() => new CombineChannelsNode();

+ 19 - 17
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -47,27 +47,27 @@ public class SeparateChannelsNode : Node, IRenderInput
         Grayscale = CreateInput(nameof(Grayscale), "GRAYSCALE", false);
     }
     
-    private void PaintRed(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintRed(RenderContext context, Canvas drawingSurface)
     {
         Paint(context, drawingSurface, _redFilter, _redGrayscaleFilter);
     }
     
-    private void PaintGreen(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintGreen(RenderContext context, Canvas drawingSurface)
     {
         Paint(context, drawingSurface, _greenFilter, _greenGrayscaleFilter);
     }
     
-    private void PaintBlue(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintBlue(RenderContext context, Canvas drawingSurface)
     {
         Paint(context, drawingSurface, _blueFilter, _blueGrayscaleFilter);
     }
     
-    private void PaintAlpha(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintAlpha(RenderContext context, Canvas drawingSurface)
     {
         Paint(context, drawingSurface, _alphaFilter, _alphaGrayscaleFilter);
     }
 
-    private void Paint(RenderContext context, DrawingSurface drawingSurface, ColorFilter colorFilter, ColorFilter grayscaleFilter)
+    private void Paint(RenderContext context, Canvas drawingSurface, ColorFilter colorFilter, ColorFilter grayscaleFilter)
     {
         if(Image.Value == null)
             return;
@@ -77,11 +77,11 @@ public class SeparateChannelsNode : Node, IRenderInput
         ColorFilter filter = grayscale ? grayscaleFilter : colorFilter; 
         _paint.ColorFilter = filter;
         
-        int saved = drawingSurface.Canvas.SaveLayer(_paint);
+        int saved = drawingSurface.SaveLayer(_paint);
         
         Image.Value.Paint(context, drawingSurface);
         
-        drawingSurface.Canvas.RestoreToCount(saved);
+        drawingSurface.RestoreToCount(saved);
     }
 
     protected override void OnExecute(RenderContext context)
@@ -94,12 +94,14 @@ public class SeparateChannelsNode : Node, IRenderInput
 
     public override Node CreateCopy() => new SeparateChannelsNode();
     RenderInputProperty IRenderInput.Background => Image;
+    
+    // TODO: Add previews
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         return null;
     }
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public bool RenderPreview(Canvas renderOn, RenderContext context, string elementToRenderName)
     {
         if (Image.Value == null)
             return false;
@@ -109,7 +111,7 @@ public class SeparateChannelsNode : Node, IRenderInput
         if (bounds == null)
             return false;
         
-        renderOn.Canvas.Save();
+        renderOn.Save();
 
         _paint.ColorFilter = Grayscale.Value ? _redGrayscaleFilter : _redFilter;
         RectD localBounds = new(bounds.Value.X, bounds.Value.Y, bounds.Value.Width / 2, bounds.Value.Height / 2);
@@ -127,23 +129,23 @@ public class SeparateChannelsNode : Node, IRenderInput
         localBounds = new(bounds.Value.X + bounds.Value.Width / 2, bounds.Value.Y + bounds.Value.Height / 2, bounds.Value.Width / 2, bounds.Value.Height / 2);
         PaintPreview(renderOn, localBounds, new VecD(bounds.Value.X + bounds.Value.Width, bounds.Value.Y + bounds.Value.Height), context);
         
-        renderOn.Canvas.Restore();
+        renderOn.Restore();
 
         return true;
     }
 
-    private void PaintPreview(DrawingSurface renderOn, RectD localBounds, VecD translation, RenderContext context)
+    private void PaintPreview(Canvas renderOn, RectD localBounds, VecD translation, RenderContext context)
     {
-        int saved = renderOn.Canvas.Save();
+        int saved = renderOn.Save();
         
-        renderOn.Canvas.ClipRect(localBounds);
-        renderOn.Canvas.SaveLayer(_paint, localBounds);
+        renderOn.ClipRect(localBounds);
+        renderOn.SaveLayer(_paint, localBounds);
 
-        renderOn.Canvas.Scale(0.5f);
-        renderOn.Canvas.Translate((float)translation.X, (float)translation.Y);
+        renderOn.Scale(0.5f);
+        renderOn.Translate((float)translation.X, (float)translation.Y);
         
         Image.Value.Paint(context, renderOn);
         
-        renderOn.Canvas.RestoreToCount(saved);
+        renderOn.RestoreToCount(saved);
     }
 }

+ 8 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -78,13 +78,14 @@ public class CreateImageNode : Node
         {
             using Paint paint = new Paint();
             paint.SetPaintable(Fill.Value);
+            paint.BlendMode = BlendMode.Src;
             surface.DrawingSurface.Canvas.DrawRect(0, 0, Size.Value.X, Size.Value.Y, paint);
         }
 
         int saved = surface.DrawingSurface.Canvas.Save();
 
         RenderContext ctx = context.Clone();
-        ctx.RenderSurface = surface.DrawingSurface;
+        ctx.RenderSurface = surface.DrawingSurface.Canvas;
         ctx.RenderOutputSize = surface.Size;
         ctx.VisibleDocumentRegion = null;
 
@@ -94,21 +95,21 @@ public class CreateImageNode : Node
             surface.DrawingSurface.Canvas.TotalMatrix.Concat(
                 Matrix3X3.CreateScale(chunkMultiplier, chunkMultiplier).Concat(ContentMatrix.Value)));
 
-        Content.Value?.Paint(ctx, surface.DrawingSurface);
+        Content.Value?.Paint(ctx, surface.DrawingSurface.Canvas);
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
         return surface;
     }
 
-    private void OnPaint(RenderContext context, DrawingSurface surface)
+    private void OnPaint(RenderContext context, Canvas surface)
     {
         if (Output.Value == null || Output.Value.IsDisposed) return;
 
-        int saved = surface.Canvas.Save();
-        surface.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
-        surface.Canvas.DrawSurface(Output.Value.DrawingSurface, 0, 0);
+        int saved = surface.Save();
+        surface.Scale((float)context.ChunkResolution.InvertedMultiplier());
+        surface.DrawSurface(Output.Value.DrawingSurface, 0, 0);
 
-        surface.Canvas.RestoreToCount(saved);
+        surface.RestoreToCount(saved);
     }
 
     private void RenderPreviews(Texture surface, RenderContext context)

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Editor/EditorInfoNode.cs

@@ -0,0 +1,28 @@
+using Drawie.Backend.Core.ColorsImpl;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Editor;
+
+[NodeInfo("EditorInfo")]
+public class EditorInfoNode : Node
+{
+    public OutputProperty<Color> PrimaryColor { get; }
+    public OutputProperty<Color> SecondaryColor { get; }
+
+    public EditorInfoNode()
+    {
+        PrimaryColor = CreateOutput<Color>("PrimaryColor", "PRIMARY_COLOR", Colors.Black);
+        SecondaryColor = CreateOutput<Color>("SecondaryColor", "SECONDARY_COLOR", Colors.White);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        PrimaryColor.Value = context.EditorData.PrimaryColor;
+        SecondaryColor.Value = context.EditorData.SecondaryColor;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new EditorInfoNode();
+    }
+}

+ 7 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs

@@ -69,7 +69,7 @@ public class OutlineNode : RenderNode, IRenderInput
         lastType = Type.Value;
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         if (Background.Value == null)
         {
@@ -89,7 +89,7 @@ public class OutlineNode : RenderNode, IRenderInput
             bool isAdjusted = context.DocumentSize == context.RenderOutputSize;
             ctx.RenderOutputSize = isAdjusted ? context.RenderOutputSize : (VecI)(context.RenderOutputSize * context.ChunkResolution.InvertedMultiplier());
 
-            Background.Value.Paint(ctx, temp.DrawingSurface);
+            Background.Value.Paint(ctx, temp.DrawingSurface.Canvas);
 
             temp.DrawingSurface.Canvas.RestoreToCount(saved);
 
@@ -106,11 +106,11 @@ public class OutlineNode : RenderNode, IRenderInput
                 temp.DrawingSurface.Canvas.RestoreToCount(saved);
             }
 
-            saved = surface.Canvas.Save();
-            surface.Canvas.SetMatrix(Matrix3X3.Identity);
-            surface.Canvas.DrawSurface(temp.DrawingSurface, 0, 0);
+            saved = surface.Save();
+            surface.SetMatrix(Matrix3X3.Identity);
+            surface.DrawSurface(temp.DrawingSurface, 0, 0);
 
-            surface.Canvas.RestoreToCount(saved);
+            surface.RestoreToCount(saved);
         }
 
         Background?.Value?.Paint(context, surface);
@@ -125,7 +125,7 @@ public class OutlineNode : RenderNode, IRenderInput
     {
         int saved = renderOn.Canvas.Save();
         renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
         renderOn.Canvas.RestoreToCount(saved);
     }
 

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs

@@ -92,7 +92,7 @@ public class PosterizationNode : RenderNode, IRenderInput
         shader = Shader.Create(shaderCode, uniforms, out _);
     }
     
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         if (Background.Value == null)
         {
@@ -117,7 +117,7 @@ public class PosterizationNode : RenderNode, IRenderInput
         }
         
         using Texture temp = Texture.ForProcessing(surface, colorSpace);
-        Background.Value.Paint(context, temp.DrawingSurface);
+        Background.Value.Paint(context, temp.DrawingSurface.Canvas);
         var snapshot = temp.DrawingSurface.Snapshot();
         
         lastImageShader?.Dispose();
@@ -136,10 +136,10 @@ public class PosterizationNode : RenderNode, IRenderInput
         temp.DrawingSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
         temp.DrawingSurface.Canvas.RestoreToCount(savedTemp);
         
-        var saved = surface.Canvas.Save();
-        surface.Canvas.SetMatrix(Matrix3X3.Identity);
-        surface.Canvas.DrawSurface(temp.DrawingSurface, 0, 0);
-        surface.Canvas.RestoreToCount(saved);
+        var saved = surface.Save();
+        surface.SetMatrix(Matrix3X3.Identity);
+        surface.DrawSurface(temp.DrawingSurface, 0, 0);
+        surface.RestoreToCount(saved);
     }
 
     public override Node CreateCopy()

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

@@ -41,13 +41,13 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         AllowHighDpiRendering = true;
     }
 
-    protected override void Paint(RenderContext context, DrawingSurface surface)
+    protected override void Paint(RenderContext context, Canvas surface)
     {
         AllowHighDpiRendering = (Background.Connection?.Node as RenderNode)?.AllowHighDpiRendering ?? true;
         base.Paint(context, surface);
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface outputSurface)
+    protected override void OnPaint(RenderContext context, Canvas outputSurface)
     {
         using var _ = DetermineTargetSurface(context, outputSurface, out var processingSurface);
 
@@ -60,7 +60,7 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         }
     }
 
-    private void DrawWithFilter(RenderContext context, DrawingSurface outputSurface, DrawingSurface processingSurface)
+    private void DrawWithFilter(RenderContext context, Canvas outputSurface, Canvas processingSurface)
     {
         _paint.SetFilters(Filter.Value);
 
@@ -70,16 +70,16 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
             return;
         }
 
-        var layer = processingSurface.Canvas.SaveLayer(_paint);
+        var layer = processingSurface.SaveLayer(_paint);
         Background.Value?.Paint(context, processingSurface);
-        processingSurface.Canvas.RestoreToCount(layer);
+        processingSurface.RestoreToCount(layer);
     }
 
-    private void HandleNonSrgbContext(RenderContext context, DrawingSurface surface, DrawingSurface targetSurface)
+    private void HandleNonSrgbContext(RenderContext context, Canvas surface, Canvas targetSurface)
     {
         using var intermediate = Texture.ForProcessing(surface, context.ProcessingColorSpace);
 
-        Background.Value?.Paint(context, intermediate.DrawingSurface);
+        Background.Value?.Paint(context, intermediate.DrawingSurface.Canvas);
 
         using var srgbSurface = Texture.ForProcessing(intermediate.Size, ColorSpace.CreateSrgb());
 
@@ -87,14 +87,14 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         srgbSurface.DrawingSurface.Canvas.DrawSurface(intermediate.DrawingSurface, 0, 0);
         srgbSurface.DrawingSurface.Canvas.Restore();
 
-        var saved = targetSurface.Canvas.Save();
-        targetSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        var saved = targetSurface.Save();
+        targetSurface.SetMatrix(Matrix3X3.Identity);
 
-        targetSurface.Canvas.DrawSurface(srgbSurface.DrawingSurface, 0, 0);
-        targetSurface.Canvas.RestoreToCount(saved);
+        targetSurface.DrawSurface(srgbSurface.DrawingSurface, 0, 0);
+        targetSurface.RestoreToCount(saved);
     }
 
-    private Texture? DetermineTargetSurface(RenderContext context, DrawingSurface outputSurface, out DrawingSurface targetSurface)
+    private Texture? DetermineTargetSurface(RenderContext context, Canvas outputSurface, out Canvas targetSurface)
     {
         targetSurface = outputSurface;
         
@@ -103,23 +103,23 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         
         Background.Value?.Paint(context, outputSurface);
         var texture = Texture.ForProcessing(outputSurface, context.ProcessingColorSpace);
-        targetSurface = texture.DrawingSurface;
+        targetSurface = texture.DrawingSurface.Canvas;
         
         return texture;
     }
 
-    private void ApplyWithMask(RenderContext context, DrawingSurface processedSurface, DrawingSurface finalSurface)
+    private void ApplyWithMask(RenderContext context, Canvas processedSurface, Canvas finalSurface)
     {
         _maskPaint.BlendMode = !InvertMask.Value ? BlendMode.DstIn : BlendMode.DstOut;
-        var maskLayer = processedSurface.Canvas.SaveLayer(_maskPaint);
+        var maskLayer = processedSurface.SaveLayer(_maskPaint);
         Mask.Value?.Paint(context, processedSurface);
-        processedSurface.Canvas.RestoreToCount(maskLayer);
+        processedSurface.RestoreToCount(maskLayer);
 
-        var saved = finalSurface.Canvas.Save();
-        finalSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        var saved = finalSurface.Save();
+        finalSurface.SetMatrix(Matrix3X3.Identity);
 
-        finalSurface.Canvas.DrawSurface(processedSurface, 0, 0);
-        finalSurface.Canvas.RestoreToCount(saved);
+        finalSurface.DrawSurface(processedSurface.Surface, 0, 0);
+        finalSurface.RestoreToCount(saved);
     }
 
     public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "") =>

+ 13 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs

@@ -26,6 +26,7 @@ public class ColorAdjustmentsFilterNode : FilterNode
     public InputProperty<double> HueValue { get; }
 
     private List<ColorFilter> filters = new List<ColorFilter>();
+    private List<ColorFilter> toDispose = new List<ColorFilter>();
     private ColorFilter lastCombinedFilter;
 
     public ColorAdjustmentsFilterNode()
@@ -57,7 +58,7 @@ public class ColorAdjustmentsFilterNode : FilterNode
 
     protected override ColorFilter? GetColorFilter(RenderContext context)
     {
-        filters.ForEach(filter => filter.Dispose());
+        toDispose.AddRange(filters);
         filters.Clear();
 
         CreateBrightnessFilter();
@@ -196,4 +197,15 @@ public class ColorAdjustmentsFilterNode : FilterNode
     {
         return new ColorAdjustmentsFilterNode();
     }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        foreach (var filter in toDispose)
+        {
+            filter.Dispose();
+        }
+
+        lastCombinedFilter?.Dispose();
+    }
 }

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

@@ -62,10 +62,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
                 paint.ImageFilter = Filters.Value?.ImageFilter;
             }
 
-            int saved = sceneContext.RenderSurface.Canvas.SaveLayer(paint);
+            int saved = sceneContext.RenderSurface.SaveLayer(paint);
             Content.Value?.Paint(sceneContext, sceneContext.RenderSurface);
 
-            sceneContext.RenderSurface.Canvas.RestoreToCount(saved);
+            sceneContext.RenderSurface.RestoreToCount(saved);
             return;
         }
 
@@ -90,20 +90,20 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         VecI size = sceneContext.RenderSurface.DeviceClipBounds.Size + sceneContext.RenderSurface.DeviceClipBounds.Pos;
         var outputWorkingSurface = RequestTexture(0, size, sceneContext.ProcessingColorSpace, true);
         outputWorkingSurface.DrawingSurface.Canvas.Save();
-        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(sceneContext.RenderSurface.Canvas.TotalMatrix);
+        outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(sceneContext.RenderSurface.TotalMatrix);
 
-        int saved = sceneContext.RenderSurface.Canvas.Save();
-        sceneContext.RenderSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        int saved = sceneContext.RenderSurface.Save();
+        sceneContext.RenderSurface.SetMatrix(Matrix3X3.Identity);
 
         blendPaint.ImageFilter = null;
         blendPaint.ColorFilter = null;
 
-        Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
+        Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface.Canvas);
 
         int saved2 = outputWorkingSurface.DrawingSurface.Canvas.Save();
         outputWorkingSurface.DrawingSurface.Canvas.Scale((float)sceneContext.ChunkResolution.InvertedMultiplier());
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext, sceneContext.ChunkResolution);
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface.Canvas, sceneContext, sceneContext.ChunkResolution);
 
         outputWorkingSurface.DrawingSurface.Canvas.RestoreToCount(saved2);
 
@@ -116,7 +116,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
             outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             if (Background.Connection.Node is IClipSource clipSource && ClipToPreviousMember)
             {
-                DrawClipSource(tempSurface.DrawingSurface, clipSource, sceneContext);
+                DrawClipSource(tempSurface.DrawingSurface.Canvas, clipSource, sceneContext);
             }
 
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
@@ -125,9 +125,9 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         AdjustPaint(useFilters);
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-        sceneContext.RenderSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
+        sceneContext.RenderSurface.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
 
-        sceneContext.RenderSurface.Canvas.RestoreToCount(saved);
+        sceneContext.RenderSurface.RestoreToCount(saved);
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
 
@@ -182,6 +182,42 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         return null;
     }
 
+    public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        RectD? bounds = null;
+        if (!IsVisible.Value)
+            return new ShapeCorners();
+
+        if (Content.Connection != null)
+        {
+            Content.Connection.Node.TraverseBackwards(
+                (n, input) =>
+                {
+                    if (n is StructureNode { IsVisible.Value: true } structureNode)
+                    {
+                        ShapeCorners childBounds = structureNode.GetTransformationCorners(frameTime);
+                        if (childBounds != default)
+                        {
+                            if (bounds == null)
+                            {
+                                bounds = childBounds.AABBBounds;
+                            }
+                            else
+                            {
+                                bounds = bounds.Value.Union(childBounds.AABBBounds);
+                            }
+                        }
+                    }
+
+                    return true;
+                }, FilterInvisibleFolders);
+
+            return bounds != null ? new ShapeCorners(bounds.Value) : new ShapeCorners();
+        }
+
+        return new ShapeCorners();
+    }
+
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)
     {
         RectD? bounds = null;
@@ -277,7 +313,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         }
     }
 
-    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto)
     {
         if (Content.Connection != null)
         {
@@ -293,4 +329,23 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
             }
         }
     }
+
+    public IReadOnlyStructureNode[] GetChildrenNodes()
+    {
+        List<IReadOnlyStructureNode> children = new();
+        if (Content.Connection != null)
+        {
+            Content.Connection.Node.TraverseBackwards((n) =>
+            {
+                if (n is IReadOnlyStructureNode structureNode)
+                {
+                    children.Add(structureNode);
+                }
+
+                return true;
+            });
+        }
+
+        return children.ToArray();
+    }
 }

+ 144 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/GradientNode.cs

@@ -0,0 +1,144 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("Gradient")]
+public class GradientNode : Node
+{
+    public InputProperty<GradientType> Type { get; }
+    public InputProperty<bool> AbsoluteCoordinates { get; }
+    public InputProperty<VecD> StartPoint { get; private set; }
+    public InputProperty<VecD> EndPoint { get; private set; }
+    public InputProperty<VecD> CenterPoint { get; private set; }
+    public InputProperty<double> Radius { get; private set; }
+    public InputProperty<double> Angle { get; private set; }
+    public InputProperty<int> StopsCount { get; }
+    public OutputProperty<GradientPaintable> Gradient { get; }
+
+    public Dictionary<InputProperty<Color>, InputProperty<float>> ColorStops { get; } = new();
+
+    public GradientNode()
+    {
+        Gradient = CreateOutput<GradientPaintable>("Gradient", "GRADIENT", null);
+        AbsoluteCoordinates = CreateInput<bool>("AbsoluteCoordinates", "ABSOLUTE_COORDINATES", false);
+        Type = CreateInput<GradientType>("Type", "TYPE", GradientType.Linear)
+            .NonOverridenChanged(UpdateType);
+        StartPoint = CreateInput<VecD>("StartPoint", "START_POINT", new VecD(0, 0));
+        EndPoint = CreateInput<VecD>("EndPoint", "END_POINT", new VecD(1, 0));
+        StopsCount = CreateInput<int>("StopsCount", "STOPS_COUNT", 2)
+            .NonOverridenChanged(_ => RegenerateStops());
+
+        GenerateStops();
+    }
+
+    private void UpdateType(GradientType type)
+    {
+        if (type == GradientType.Linear)
+        {
+            RemoveInputProperty(CenterPoint);
+            RemoveInputProperty(Radius);
+            RemoveInputProperty(Angle);
+            if (!HasInputProperty(StartPoint.InternalPropertyName))
+            {
+                StartPoint = CreateInput<VecD>("StartPoint", "START_POINT", new VecD(0, 0));
+            }
+
+            if (!HasInputProperty(EndPoint.InternalPropertyName))
+            {
+                EndPoint = CreateInput<VecD>("EndPoint", "END_POINT", new VecD(1, 0));
+            }
+        }
+        else if (type == GradientType.Radial)
+        {
+            RemoveInputProperty(StartPoint);
+            RemoveInputProperty(EndPoint);
+            RemoveInputProperty(Angle);
+            if (!HasInputProperty("CenterPoint"))
+            {
+                CenterPoint = CreateInput<VecD>("CenterPoint", "CENTER_POINT", new VecD(0.5, 0.5));
+            }
+
+            if (!HasInputProperty("Radius"))
+            {
+                Radius = CreateInput<double>("Radius", "RADIUS", 0.5).WithRules(x => x.Min(0d));
+            }
+        }
+        else if (type == GradientType.Conical)
+        {
+            RemoveInputProperty(StartPoint);
+            RemoveInputProperty(EndPoint);
+            RemoveInputProperty(Radius);
+            if (!HasInputProperty("CenterPoint"))
+            {
+                CenterPoint = CreateInput<VecD>("CenterPoint", "CENTER_POINT", new VecD(0.5, 0.5));
+            }
+
+            if (!HasInputProperty("Angle"))
+            {
+                Angle = CreateInput<double>("Angle", "ANGLE", 0);
+            }
+        }
+    }
+
+    private void RegenerateStops()
+    {
+        if (StopsCount.Value < ColorStops.Count)
+        {
+            int diff = ColorStops.Count - StopsCount.Value;
+            var keysToRemove = ColorStops.Keys.TakeLast(diff).ToList();
+            foreach (var key in keysToRemove)
+            {
+                RemoveInputProperty(key);
+                RemoveInputProperty(ColorStops[key]);
+                ColorStops.Remove(key);
+            }
+        }
+
+        GenerateStops();
+    }
+
+    private void GenerateStops()
+    {
+        int startIndex = ColorStops.Count;
+        for (int i = startIndex; i < StopsCount.Value; i++)
+        {
+            var colorInput = CreateInput<Color>($"ColorStopColor_{i + 1}", "COLOR_STOP_COLOR",
+                Drawie.Backend.Core.ColorsImpl.Colors.White);
+            var positionInput = CreateInput<float>($"ColorStopPosition_{i + 1}", $"COLOR_STOP_POSITION",
+                i / (float)(StopsCount.Value - 1));
+            ColorStops[colorInput] = positionInput;
+        }
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var stops = ColorStops.Select(kvp => new GradientStop(kvp.Key.Value, kvp.Value.Value)).ToList();
+        Gradient.Value = GenerateGradient(Type.Value, stops);
+    }
+
+    private GradientPaintable GenerateGradient(GradientType type, List<GradientStop> stops)
+    {
+        return type switch
+        {
+            GradientType.Linear => new LinearGradientPaintable(StartPoint.Value, EndPoint.Value, stops) { AbsoluteValues = AbsoluteCoordinates.Value },
+            GradientType.Radial => new RadialGradientPaintable(CenterPoint.Value, Radius.Value, stops) { AbsoluteValues = AbsoluteCoordinates.Value },
+            GradientType.Conical => new SweepGradientPaintable(CenterPoint.Value, Angle.Value, stops) { AbsoluteValues = AbsoluteCoordinates.Value },
+            _ => throw new NotImplementedException("Unknown gradient type")
+        };
+    }
+
+    public override Node CreateCopy()
+    {
+        return new GradientNode();
+    }
+}
+
+public enum GradientType
+{
+    Linear,
+    Radial,
+    Conical
+}

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

@@ -27,7 +27,7 @@ public sealed class MaskNode : RenderNode, IRenderInput
         Output.FirstInChain = null;
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         if (Background.Value == null)
         {
@@ -43,9 +43,9 @@ public sealed class MaskNode : RenderNode, IRenderInput
 
         maskPaint.BlendMode = !Invert.Value ? BlendMode.DstIn : BlendMode.DstOut;
 
-        int layer = surface.Canvas.SaveLayer(maskPaint);
+        int layer = surface.SaveLayer(maskPaint);
         Mask.Value.Paint(context, surface);
-        surface.Canvas.RestoreToCount(layer);
+        surface.RestoreToCount(layer);
     }
 
     public override Node CreateCopy()

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

@@ -47,7 +47,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
+        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightLatestBounds();
     }
 
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)
@@ -87,47 +87,47 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     }
 
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         bool useFilters = true)
     {
-        int scaled = workingSurface.Canvas.Save();
+        int scaled = workingSurface.Save();
         float multiplier = (float)ctx.ChunkResolution.InvertedMultiplier();
-        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime));
+        workingSurface.Translate(GetScenePosition(ctx.FrameTime));
 
         base.DrawLayerInScene(ctx, workingSurface, useFilters);
 
-        workingSurface.Canvas.RestoreToCount(scaled);
+        workingSurface.RestoreToCount(scaled);
     }
 
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         ChunkResolution resolution,
         bool useFilters, Paint paint)
     {
-        int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime) * resolution.Multiplier());
-        workingSurface.Canvas.Scale((float)resolution.Multiplier());
+        int scaled = workingSurface.Save();
+        workingSurface.Translate(GetScenePosition(ctx.FrameTime) * resolution.Multiplier());
+        workingSurface.Scale((float)resolution.Multiplier());
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
-        workingSurface.Canvas.RestoreToCount(scaled);
+        workingSurface.RestoreToCount(scaled);
     }
 
-    protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, Canvas workingSurface,
         Paint paint)
     {
         DrawLayer(workingSurface, paint, ctx, false);
     }
 
-    protected override void DrawWithFilters(SceneObjectRenderContext context, DrawingSurface workingSurface,
+    protected override void DrawWithFilters(SceneObjectRenderContext context, Canvas workingSurface,
         Paint paint)
     {
         DrawLayer(workingSurface, paint, context, true);
     }
 
-    private void DrawLayer(DrawingSurface workingSurface, Paint paint, SceneObjectRenderContext ctx, bool saveLayer)
+    private void DrawLayer(Canvas workingSurface, Paint paint, SceneObjectRenderContext ctx, bool saveLayer)
     {
-        int saved = workingSurface.Canvas.Save();
+        int saved = workingSurface.Save();
 
         var sceneSize = GetSceneSize(ctx.FrameTime);
         RectI latestSize = new(0, 0, layerImage.LatestSize.X, layerImage.LatestSize.Y);
@@ -136,12 +136,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         VecD topLeft = region.TopLeft - sceneSize / 2;
 
         topLeft *= ctx.ChunkResolution.Multiplier();
-        workingSurface.Canvas.Scale((float)ctx.ChunkResolution.InvertedMultiplier());
+        workingSurface.Scale((float)ctx.ChunkResolution.InvertedMultiplier());
         var img = GetLayerImageAtFrame(ctx.FrameTime.Frame);
 
         if (saveLayer)
         {
-            workingSurface.Canvas.SaveLayer(paint);
+            workingSurface.SaveLayer(paint);
         }
 
         if (!ctx.FullRerender)
@@ -159,7 +159,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 workingSurface, topLeft, saveLayer ? null : paint, ctx.DesiredSamplingOptions);
         }
 
-        workingSurface.Canvas.RestoreToCount(saved);
+        workingSurface.RestoreToCount(saved);
     }
 
     public override RectD? GetPreviewBounds(RenderContext context, string elementFor = "")
@@ -275,7 +275,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         img.DrawCommittedRegionOn(
             new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
             context.ChunkResolution,
-            renderOnto, VecI.Zero, replacePaint, context.DesiredSamplingOptions);
+            renderOnto.Canvas, VecI.Zero, replacePaint, context.DesiredSamplingOptions);
 
         renderOnto.Canvas.RestoreToCount(saved);
     }

+ 33 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KeyboardInfoNode.cs

@@ -0,0 +1,33 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("KeyboardInfo")]
+public class KeyboardInfoNode : Node
+{
+    public OutputProperty<bool> IsCtrlPressed { get; }
+    public OutputProperty<bool> IsShiftPressed { get; }
+    public OutputProperty<bool> IsAltPressed { get; }
+    public OutputProperty<bool> IsMetaPressed { get; }
+
+    public KeyboardInfoNode()
+    {
+        IsCtrlPressed = CreateOutput<bool>("IsCtrlPressed", "IS_CTRL_PRESSED", false);
+        IsShiftPressed = CreateOutput<bool>("IsShiftPressed", "IS_SHIFT_PRESSED", false);
+        IsAltPressed = CreateOutput<bool>("IsAltPressed", "IS_ALT_PRESSED", false);
+        IsMetaPressed = CreateOutput<bool>("IsMetaPressed", "IS_META_PRESSED", false);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        IsCtrlPressed.Value = context.KeyboardInfo.IsCtrlPressed;
+        IsShiftPressed.Value = context.KeyboardInfo.IsShiftPressed;
+        IsAltPressed.Value = context.KeyboardInfo.IsAltPressed;
+        IsMetaPressed.Value = context.KeyboardInfo.IsMetaPressed;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new KeyboardInfoNode();
+    }
+}

+ 45 - 30
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs

@@ -36,8 +36,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             sceneContext.TargetPropertyOutput == Output);
     }
 
-    private void RenderContent(SceneObjectRenderContext context, DrawingSurface renderOnto, bool useFilters)
+    private void RenderContent(SceneObjectRenderContext context, Canvas renderOnto, bool useFilters)
     {
+        if (renderOnto == null)
+            return;
+
         if (!HasOperations())
         {
             if (Background.Value != null)
@@ -61,9 +64,14 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
                     context.ProcessingColorSpace, 22);
 
-                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters,
+                var originalSurface = context.RenderSurface;
+                context.RenderSurface = tempSurface.DrawingSurface.Canvas;
+
+                DrawLayerOnTexture(context, tempSurface.DrawingSurface.Canvas, context.ChunkResolution, useFilters,
                     targetPaint);
 
+                context.RenderSurface = originalSurface;
+
                 blendPaint.SetFilters(null);
                 DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution,
                     context.DesiredSamplingOptions);
@@ -75,7 +83,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         VecI size = AllowHighDpiRendering
             ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
             : context.RenderOutputSize;
-        int saved = renderOnto.Canvas.Save();
+        int saved = renderOnto.Save();
 
         var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
 
@@ -86,8 +94,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         outputWorkingSurface.DrawingSurface.Canvas.Save();
         if (AllowHighDpiRendering)
         {
-            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.Canvas.TotalMatrix);
-            renderOnto.Canvas.SetMatrix(Matrix3X3.Identity);
+            outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(renderOnto.TotalMatrix);
+            renderOnto.SetMatrix(Matrix3X3.Identity);
         }
 
         using var paint = new Paint
@@ -95,9 +103,15 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             Color = new Color(255, 255, 255, 255), BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
         };
 
-        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface, adjustedResolution, false, paint);
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, context, adjustedResolution);
+        var originalRenderSurface = context.RenderSurface;
+        context.RenderSurface = outputWorkingSurface.DrawingSurface.Canvas;
+
+        DrawLayerOnTexture(context, outputWorkingSurface.DrawingSurface.Canvas, adjustedResolution, false, paint);
+
+        context.RenderSurface = originalRenderSurface;
+
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface.Canvas, context, adjustedResolution);
 
         if (Background.Value != null)
         {
@@ -118,7 +132,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             {
-                DrawClipSource(tempSurface.DrawingSurface, clipSource, context);
+                DrawClipSource(tempSurface.DrawingSurface.Canvas, clipSource, context);
             }
 
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
@@ -137,52 +151,52 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, adjustedResolution,
             context.DesiredSamplingOptions);
 
-        renderOnto.Canvas.RestoreToCount(saved);
+        renderOnto.RestoreToCount(saved);
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
 
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         ChunkResolution resolution,
         bool useFilters, Paint paint)
     {
-        int scaled = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Scale((float)resolution.Multiplier());
+        int scaled = workingSurface.Save();
+        workingSurface.Scale((float)resolution.Multiplier());
 
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
-        workingSurface.Canvas.RestoreToCount(scaled);
+        workingSurface.RestoreToCount(scaled);
     }
 
-    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution,
+    private void DrawWithResolution(DrawingSurface source, Canvas target, ChunkResolution resolution,
         SamplingOptions sampling)
     {
-        int scaled = target.Canvas.Save();
+        int scaled = target.Save();
         float multiplier = (float)resolution.InvertedMultiplier();
-        target.Canvas.Scale(multiplier, multiplier);
+        target.Scale(multiplier, multiplier);
 
         if (sampling == SamplingOptions.Default)
         {
-            target.Canvas.DrawSurface(source, 0, 0, blendPaint);
+            target.DrawSurface(source, 0, 0, blendPaint);
         }
         else
         {
             using var snapshot = source.Snapshot();
-            target.Canvas.DrawImage(snapshot, 0, 0, sampling, blendPaint);
+            target.DrawImage(snapshot, 0, 0, sampling, blendPaint);
         }
 
-        target.Canvas.RestoreToCount(scaled);
+        target.RestoreToCount(scaled);
     }
 
 
     protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         bool useFilters = true)
     {
         DrawLayerOnto(ctx, workingSurface, useFilters, blendPaint);
     }
 
-    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected void DrawLayerOnto(SceneObjectRenderContext ctx, Canvas workingSurface,
         bool useFilters, Paint paint)
     {
         paint.Color = paint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
@@ -192,13 +206,13 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         int saved = -1;
         if (!ctx.ProcessingColorSpace.IsSrgb && useFilters && Filters.Value != null)
         {
-            saved = workingSurface.Canvas.Save();
+            saved = workingSurface.Save();
 
             tex = Texture.ForProcessing(workingSurface,
                 ColorSpace.CreateSrgb());
-            workingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
-
-            targetSurface = tex.DrawingSurface;
+            workingSurface.SetMatrix(Matrix3X3.Identity);
+            ctx.RenderSurface = tex.DrawingSurface.Canvas;
+            targetSurface = tex.DrawingSurface.Canvas;
         }
 
         if (useFilters && Filters.Value != null)
@@ -214,16 +228,17 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
         if (targetSurface != workingSurface)
         {
-            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            workingSurface.DrawSurface(targetSurface.Surface, 0, 0);
             tex.Dispose();
-            workingSurface.Canvas.RestoreToCount(saved);
+            workingSurface.RestoreToCount(saved);
+            ctx.RenderSurface = workingSurface;
         }
     }
 
-    protected abstract void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected abstract void DrawWithoutFilters(SceneObjectRenderContext ctx, Canvas workingSurface,
         Paint paint);
 
-    protected abstract void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected abstract void DrawWithFilters(SceneObjectRenderContext ctx, Canvas workingSurface,
         Paint paint);
 
     protected Texture TryInitWorkingSurface(VecI imageSize, ChunkResolution resolution, ColorSpace processingCs, int id)
@@ -249,7 +264,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         return workingSurface;
     }
 
-    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto)
     {
         RenderContent(context, drawOnto, false);
     }

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

@@ -98,7 +98,7 @@ public class MathNode : Node
             MathNodeMode.GreaterThanOrEqual => xConst >= yConst ? 1 : 0,
             MathNodeMode.LessThan => xConst < yConst ? 1 : 0,
             MathNodeMode.LessThanOrEqual => xConst <= yConst ? 1 : 0,
-            MathNodeMode.Compare => Math.Abs(xConst - yConst) < zConst ? 1 : 0,
+            MathNodeMode.Compare => Math.Abs(xConst - yConst) <= zConst ? 1 : 0,
             MathNodeMode.Power => Math.Pow(xConst, yConst),
             MathNodeMode.Logarithm => Math.Log(xConst, yConst),
             MathNodeMode.NaturalLogarithm => Math.Log(xConst),

+ 21 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

@@ -5,6 +5,7 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
@@ -13,36 +14,51 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 {
     public RenderInputProperty Background { get; }
     public FuncInputProperty<Float3x3> Input { get; }
+    public InputProperty<ShapeVectorData> InputVector { get; }
     public FuncOutputProperty<Float3x3> Matrix { get; }
+    public OutputProperty<ShapeVectorData> OutputVector { get; }
 
     public Matrix3X3BaseNode()
     {
         Background = CreateRenderInput("Background", "IMAGE");
         Input = CreateFuncInput<Float3x3>("Input", "INPUT_MATRIX",
             new Float3x3("") { ConstantValue = Matrix3X3.Identity });
+        InputVector = CreateInput<ShapeVectorData>("VectorShape", "SHAPE_LABEL", null);
         Matrix = CreateFuncOutput<Float3x3>("Matrix", "OUTPUT_MATRIX",
             (c) => CalculateMatrix(c, c.GetValue(Input)));
+
+        OutputVector = CreateOutput<ShapeVectorData>("OutputVector", "VECTOR", null);
         Output.FirstInChain = null;
         AllowHighDpiRendering = true;
     }
 
     protected override void OnExecute(RenderContext context)
     {
+        if (InputVector.Value != null)
+        {
+            var clone = InputVector.Value.Clone() as ShapeVectorData;
+            Matrix3X3 mtx = Matrix.Value.Invoke(FuncContext.NoContext).GetConstant() as Matrix3X3? ??
+                            Matrix3X3.Identity;
+
+            clone.TransformationMatrix = clone.TransformationMatrix.PostConcat(mtx);
+            OutputVector.Value = clone;
+        }
+
         if (Background.Value == null)
             return;
 
         base.OnExecute(context);
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
-        int layer = surface.Canvas.Save();
+        int layer = surface.Save();
 
         Float3x3 mtx = Matrix.Value.Invoke(FuncContext.NoContext);
 
         Matrix3X3 constant = mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity;
-        surface.Canvas.SetMatrix(
-            surface.Canvas.TotalMatrix.Concat(constant));
+        surface.SetMatrix(
+            surface.TotalMatrix.Concat(constant));
 
         var clonedCtx = context.Clone();
         if (clonedCtx.VisibleDocumentRegion.HasValue)
@@ -56,7 +72,7 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
             Background.Value?.Paint(clonedCtx, surface);
         }
 
-        surface.Canvas.RestoreToCount(layer);
+        surface.RestoreToCount(layer);
     }
 
     public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")

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

@@ -34,7 +34,7 @@ public class MergeNode : RenderNode
     }
 
 
-    protected override void OnPaint(RenderContext context, DrawingSurface target)
+    protected override void OnPaint(RenderContext context, Canvas target)
     {
         if (Top.Value == null && Bottom.Value == null)
         {
@@ -50,19 +50,19 @@ public class MergeNode : RenderNode
         Merge(target, context);
     }
 
-    private void Merge(DrawingSurface target, RenderContext context)
+    private void Merge(Canvas target, RenderContext context)
     {
         if (Bottom.Value != null && Top.Value != null)
         {
-            int saved = target.Canvas.SaveLayer();
+            int saved = target.SaveLayer();
             Bottom.Value?.Paint(context, target);
-            target.Canvas.RestoreToCount(saved);
+            target.RestoreToCount(saved);
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-            target.Canvas.SaveLayer(paint);
+            target.SaveLayer(paint);
 
             Top.Value?.Paint(context, target);
-            target.Canvas.RestoreToCount(saved);
+            target.RestoreToCount(saved);
             return;
         }
 
@@ -82,7 +82,7 @@ public class MergeNode : RenderNode
             return;
         }
 
-        Merge(renderOn, context);
+        Merge(renderOn.Canvas, context);
     }
 
     public override void Dispose()

+ 9 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -19,12 +19,12 @@ public class ModifyImageLeftNode : Node, IPairNode
     public FuncOutputProperty<Float2> Coordinate { get; }
 
     public FuncOutputProperty<Half4> Color { get; }
-    
+
     public InputProperty<ColorSampleMode> SampleMode { get; }
     public InputProperty<bool> NormalizeCoordinates { get; }
 
     public Guid OtherNode { get; set; }
-    
+
     public ModifyImageLeftNode()
     {
         Image = CreateInput<Texture?>("Surface", "IMAGE", null);
@@ -33,17 +33,18 @@ public class ModifyImageLeftNode : Node, IPairNode
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
         NormalizeCoordinates = CreateInput("NormalizeCoordinates", "NORMALIZE_COORDINATES", true);
     }
-    
+
     private Half4 GetColor(FuncContext context)
     {
         context.ThrowOnMissingContext();
-        
-        if(Image.Value == null)
+
+        if (Image.Value == null)
         {
             return new Half4("") { ConstantValue = Colors.Transparent };
         }
 
-        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value, NormalizeCoordinates.Value);
+        return context.SampleSurface(Image.Value.DrawingSurface, context.SamplePosition, SampleMode.Value,
+            NormalizeCoordinates.Value);
     }
 
     protected override void OnExecute(RenderContext context)
@@ -80,12 +81,12 @@ public class ModifyImageLeftNode : Node, IPairNode
 
     public bool RenderPreview(DrawingSurface renderOn)
     {
-        if(Image.Value is null)
+        if (Image.Value is null)
         {
             return false;
         }
 
-        renderOn.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0); 
+        renderOn.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0);
         return true;
     }
 }

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

@@ -38,7 +38,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         RendersInAbsoluteCoordinates = true;
     }
 
-    protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
+    protected override void OnPaint(RenderContext renderContext, Canvas targetSurface)
     {
         if (OtherNode == null || OtherNode == default)
         {
@@ -96,7 +96,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             color.VariableName = "color";
             builder.AddUniform(color.VariableName, color.ConstantValue);
-            builder.ReturnVar(color, false); // Do not premultiply, since we are modifying already premultiplied image
+            builder.ReturnVar(color, false);
         }
 
         string sksl = builder.ToSkSl();
@@ -111,7 +111,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
+        targetSurface.DrawRect(0, 0, size.Value.X, size.Value.Y, drawingPaint);
         builder.Dispose();
     }
 

+ 479 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NestedDocumentNode.cs

@@ -0,0 +1,479 @@
+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;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo(NodeId)]
+public class NestedDocumentNode : LayerNode, IInputDependentOutputs, ITransformableObject, IRasterizable,
+    IVariableSampling
+{
+    public const int MaxRecursionDepth = 5;
+    public const string DocumentPropertyName = "Document";
+    public const string NodeId = "NestedDocument";
+    private DocumentReference? lastDocument;
+    public InputProperty<DocumentReference> NestedDocument { get; }
+
+    public InputProperty<bool> BilinearSampling { get; }
+
+    public OutputProperty<IReadOnlyNodeGraph> Graph { get; }
+
+    public Matrix3X3 TransformationMatrix { get; set; } = Matrix3X3.Identity;
+
+    public RectD TransformedAABB => new ShapeCorners(NestedDocument.Value?.DocumentInstance?.Size / 2f ?? VecD.Zero,
+            NestedDocument.Value?.DocumentInstance?.Size ?? VecD.Zero)
+        .WithMatrix(TransformationMatrix).AABBBounds;
+
+    private IReadOnlyDocument? Instance => NestedDocument.Value?.DocumentInstance;
+
+    private string[] builtInOutputs;
+    private string[] builtInInputs;
+
+    private ExposeValueNode[]? cachedExposeNodes;
+    private BrushOutputNode[]? brushOutputNodes;
+    private IReadOnlyNode[] toExecute;
+
+    public NestedDocumentNode()
+    {
+        NestedDocument = CreateInput<DocumentReference>(DocumentPropertyName, "DOCUMENT", null)
+            .NonOverridenChanged(DocumentChanged);
+        NestedDocument.ConnectionChanged += NestedDocumentOnConnectionChanged;
+        BilinearSampling = CreateInput<bool>("BilinearSampling", "BILINEAR_SAMPLING", false);
+        Graph = CreateOutput<IReadOnlyNodeGraph>("Graph", "GRAPH", null);
+        AllowHighDpiRendering = true;
+
+        builtInOutputs = OutputProperties.Select(x => x.InternalPropertyName).ToArray();
+        builtInInputs = InputProperties.Select(x => x.InternalPropertyName).ToArray();
+    }
+
+    protected override int GetContentCacheHash()
+    {
+        return HashCode.Combine(base.GetContentCacheHash(), TransformationMatrix);
+    }
+
+    private void NestedDocumentOnConnectionChanged()
+    {
+        if (NestedDocument.Value == null && NestedDocument.Connection != null) return;
+
+        DocumentChanged(NestedDocument.Value);
+    }
+
+    private void DocumentChanged(DocumentReference document)
+    {
+        lastDocument = NestedDocument.Value;
+        if (document?.DocumentInstance == null)
+        {
+            ClearOutputProperties();
+            ClearInputProperties();
+            cachedExposeNodes = null;
+            return;
+        }
+
+        cachedExposeNodes = document.DocumentInstance.NodeGraph.AllNodes
+            .OfType<ExposeValueNode>().ToArray();
+
+        brushOutputNodes = document.DocumentInstance.NodeGraph.AllNodes
+            .OfType<BrushOutputNode>().ToArray();
+
+        toExecute = cachedExposeNodes.Concat<IReadOnlyNode>(brushOutputNodes).Concat([Instance?.NodeGraph.OutputNode])
+            .ToArray();
+
+        Instance?.NodeGraph.Execute(cachedExposeNodes.Concat<IReadOnlyNode>(brushOutputNodes), new RenderContext(null,
+            0,
+            ChunkResolution.Full,
+            Instance.Size, Instance.Size,
+            Instance.ProcessingColorSpace,
+            SamplingOptions.Default,
+            Instance.NodeGraph) { FullRerender = true });
+
+        foreach (var input in cachedExposeNodes)
+        {
+            if (input.Name.Value == Output.InternalPropertyName)
+                continue;
+
+            if (OutputProperties.Any(x =>
+                    x.InternalPropertyName == input.Name.Value))
+                continue;
+
+            AddOutputProperty(new OutputProperty(this, input.Name.Value, input.Name.Value, input.Value.Value,
+                input.Value.Value?.GetType() ?? typeof(object)));
+        }
+
+        foreach (var brushOutput in brushOutputNodes)
+        {
+            if (OutputProperties.Any(x =>
+                    brushOutput.InputProperties.Any(prop =>
+                        $"{brushOutput.BrushName}_{prop.InternalPropertyName}" == x.InternalPropertyName)))
+                continue;
+
+            foreach (var output in brushOutput.InputProperties)
+            {
+                AddOutputProperty(new OutputProperty(this, $"{brushOutput.BrushName}_{output.InternalPropertyName}",
+                    output.DisplayName,
+                    output.Value, output.ValueType));
+            }
+        }
+
+        foreach (var variable in document.DocumentInstance.NodeGraph.Blackboard.Variables)
+        {
+            if (InputProperties.Any(x =>
+                    x.InternalPropertyName == variable.Key && x.ValueType == variable.Value.Type))
+            {
+                continue;
+            }
+
+            if(!variable.Value.IsExposed)
+                continue;
+
+            AddInputProperty(new InputProperty(this, variable.Key, variable.Key, variable.Value.Value,
+                variable.Value.Type));
+        }
+
+        for (int i = OutputProperties.Count - 1; i >= 0; i--)
+        {
+            var output = OutputProperties[i];
+            if (builtInOutputs.Contains(output.InternalPropertyName))
+                continue;
+
+            bool shouldRemove = cachedExposeNodes.All(x => x.Name.Value != output.InternalPropertyName) &&
+                                brushOutputNodes.All(brushOutput => brushOutput.InputProperties
+                                    .All(prop =>
+                                        $"{brushOutput.BrushName}_{prop.InternalPropertyName}" !=
+                                        output.InternalPropertyName));
+
+            if (shouldRemove)
+            {
+                RemoveOutputProperty(output);
+            }
+        }
+
+        for (int i = InputProperties.Count - 1; i >= 0; i--)
+        {
+            var input = InputProperties[i];
+            if (builtInInputs.Contains(input.InternalPropertyName))
+                continue;
+
+            bool shouldRemove = document.DocumentInstance.NodeGraph.Blackboard.Variables
+                .All(x => x.Key != input.InternalPropertyName ||
+                          x.Value.Type != input.ValueType) ||
+                              !document.DocumentInstance.NodeGraph.Blackboard.Variables[input.InternalPropertyName]
+                                  .IsExposed;
+
+            if (shouldRemove)
+            {
+                RemoveInputProperty(input);
+            }
+        }
+    }
+
+    private void ClearOutputProperties()
+    {
+        var toRemove = OutputProperties
+            .Where(x => !builtInOutputs.Contains(x.InternalPropertyName))
+            .ToList();
+        foreach (var property in toRemove)
+        {
+            RemoveOutputProperty(property);
+        }
+    }
+
+    private void ClearInputProperties()
+    {
+        var toRemove = InputProperties
+            .Where(x => !builtInInputs.Contains(x.InternalPropertyName))
+            .ToList();
+        foreach (var property in toRemove)
+        {
+            RemoveInputProperty(property);
+        }
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+
+        if (Instance is null)
+            return;
+
+        if (Instance != lastDocument?.DocumentInstance)
+        {
+            DocumentChanged(NestedDocument.Value);
+        }
+
+        foreach (var blackboardVariable in Instance?.NodeGraph.Blackboard.Variables)
+        {
+            var input = InputProperties.FirstOrDefault(x =>
+                x.InternalPropertyName == blackboardVariable.Key &&
+                x.ValueType == blackboardVariable.Value.Type);
+
+            if (input is null || blackboardVariable.Value is not Variable variable)
+                continue;
+
+            variable.Value = input.Value;
+        }
+
+        var clonedContext = context.Clone();
+        if (clonedContext.CloneDepth >= MaxRecursionDepth)
+        {
+            return;
+        }
+
+        clonedContext.Graph = Instance?.NodeGraph;
+        clonedContext.DocumentSize = Instance.Size;
+        clonedContext.ProcessingColorSpace = Instance?.ProcessingColorSpace;
+        clonedContext.VisibleDocumentRegion = null;
+        clonedContext.RenderSurface = null;
+
+        Instance?.NodeGraph.Execute(toExecute, clonedContext);
+
+        if (AnyConnectionExists())
+        {
+            foreach (var output in OutputProperties)
+            {
+                if (output.InternalPropertyName == Output.InternalPropertyName)
+                    continue;
+
+                var correspondingExposeNode = cachedExposeNodes?
+                    .FirstOrDefault(x => x.Name.Value == output.InternalPropertyName &&
+                                         x.Value.ValueType == output.ValueType);
+
+                if (correspondingExposeNode is null)
+                {
+                    var correspondingBrushNode = brushOutputNodes?
+                        .FirstOrDefault(brushOutput => brushOutput.InputProperties
+                            .Any(prop =>
+                                $"{brushOutput.BrushName}_{prop.InternalPropertyName}" == output.InternalPropertyName &&
+                                prop.ValueType == output.ValueType));
+                    if (correspondingBrushNode is not null)
+                    {
+                        var correspondingProp = correspondingBrushNode.InputProperties
+                            .First(prop =>
+                                $"{correspondingBrushNode.BrushName}_{prop.InternalPropertyName}" ==
+                                output.InternalPropertyName &&
+                                prop.ValueType == output.ValueType);
+                        output.Value = correspondingProp.Value;
+                    }
+
+                    continue;
+                }
+
+                output.Value = correspondingExposeNode.Value.Value;
+            }
+        }
+
+        Graph.Value = Instance.NodeGraph;
+    }
+
+    protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, Canvas workingSurface, Paint paint)
+    {
+        if (NestedDocument.Value is null)
+            return;
+
+        int saved;
+        if (paint.IsOpaqueStandardNonBlendingPaint)
+        {
+            saved = workingSurface.Save();
+        }
+        else
+        {
+            saved = workingSurface.SaveLayer(paint);
+        }
+
+        workingSurface.SetMatrix(workingSurface.TotalMatrix.Concat(TransformationMatrix));
+
+        ExecuteNested(ctx);
+
+        workingSurface.RestoreToCount(saved);
+    }
+
+
+    protected override void DrawWithFilters(SceneObjectRenderContext ctx, Canvas workingSurface, Paint paint)
+    {
+        if (NestedDocument.Value is null)
+            return;
+
+        int saved = workingSurface.SaveLayer(paint);
+
+        workingSurface.SetMatrix(workingSurface.TotalMatrix.Concat(TransformationMatrix));
+
+        ExecuteNested(ctx);
+
+        workingSurface.RestoreToCount(saved);
+    }
+
+    public void Rasterize(Canvas surface, Paint paint, int atFrame)
+    {
+        if (NestedDocument.Value is null)
+            return;
+
+        int layer;
+        if (paint is { IsOpaqueStandardNonBlendingPaint: false })
+        {
+            layer = surface.SaveLayer(paint);
+        }
+        else
+        {
+            layer = surface.Save();
+        }
+
+        surface.SetMatrix(surface.TotalMatrix.Concat(TransformationMatrix));
+
+        RenderContext context = new(
+            surface, atFrame, ChunkResolution.Full,
+            surface.DeviceClipBounds.Size,
+            Instance.Size,
+            Instance.ProcessingColorSpace,
+            BilinearSampling.Value ? SamplingOptions.Bilinear : SamplingOptions.Default,
+            Instance.NodeGraph) { FullRerender = true, };
+
+        ExecuteNested(context);
+
+        surface.RestoreToCount(layer);
+    }
+
+    private void ExecuteNested(RenderContext ctx)
+    {
+        var clonedContext = ctx.Clone();
+        if (clonedContext.CloneDepth >= MaxRecursionDepth)
+        {
+            return;
+        }
+
+        clonedContext.Graph = Instance?.NodeGraph;
+        clonedContext.DocumentSize = Instance?.Size ?? VecI.Zero;
+        clonedContext.ProcessingColorSpace = Instance?.ProcessingColorSpace;
+        clonedContext.RenderOutputSize = clonedContext.DocumentSize;
+        clonedContext.DesiredSamplingOptions =
+            BilinearSampling.Value ? SamplingOptions.Bilinear : SamplingOptions.Default;
+        if (clonedContext.VisibleDocumentRegion.HasValue)
+        {
+            clonedContext.VisibleDocumentRegion =
+                (RectI)new ShapeCorners((RectD)clonedContext.VisibleDocumentRegion.Value)
+                    .WithMatrix(TransformationMatrix.Invert()).AABBBounds;
+        }
+
+        var outputNode = Instance?.NodeGraph.AllNodes.OfType<BrushOutputNode>().FirstOrDefault() ??
+                         Instance?.NodeGraph.OutputNode;
+
+        Instance?.NodeGraph.Execute(outputNode, clonedContext);
+    }
+
+    protected override bool ShouldRenderPreview(string elementToRenderName)
+    {
+        if (IsDisposed)
+        {
+            return false;
+        }
+
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            return base.ShouldRenderPreview(elementToRenderName);
+        }
+
+        return NestedDocument.Value != null;
+    }
+
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        return TransformedAABB;
+    }
+
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (renderOn is null) return;
+
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            base.RenderPreview(renderOn, context, elementToRenderName);
+            return;
+        }
+
+        Paint(context, renderOn.Canvas);
+    }
+
+    public override RectD? GetTightBounds(KeyFrameTime frameTime)
+    {
+        return TransformedAABB;
+    }
+
+    public override RectD? GetApproxBounds(KeyFrameTime frameTime)
+    {
+        return TransformedAABB;
+    }
+
+    public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return new ShapeCorners(Instance?.Size / 2f ?? VecD.Zero, Instance?.Size ?? VecD.Zero)
+            .WithMatrix(TransformationMatrix);
+    }
+
+    public override void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
+    {
+        base.SerializeAdditionalData(target, additionalData);
+        additionalData["lastDocument"] = lastDocument;
+        additionalData["TransformationMatrix"] = TransformationMatrix;
+    }
+
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data,
+        List<IChangeInfo> infos)
+    {
+        base.DeserializeAdditionalData(target, data, infos);
+        if (data.TryGetValue("lastDocument", out var doc) && doc is DocumentReference document)
+        {
+            DocumentChanged(document); // restore outputs
+            infos.Add(NodeOutputsChanged_ChangeInfo.FromNode(this));
+        }
+
+        if (data.TryGetValue("TransformationMatrix", out var matrix) && matrix is Matrix3X3 mat)
+        {
+            TransformationMatrix = mat;
+        }
+    }
+
+    public override VecD GetScenePosition(KeyFrameTime frameTime)
+    {
+        return TransformedAABB.Center;
+    }
+
+    public override VecD GetSceneSize(KeyFrameTime frameTime)
+    {
+        return TransformedAABB.Size;
+    }
+
+    public void UpdateOutputs()
+    {
+        DocumentChanged(NestedDocument.Value);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new NestedDocumentNode() { TransformationMatrix = this.TransformationMatrix };
+    }
+
+    public override void Dispose()
+    {
+        Graph.Value = null; // Prevent disposing nested document's graph
+        base.Dispose();
+    }
+
+    private bool AnyConnectionExists()
+    {
+        foreach (var output in OutputProperties)
+        {
+            if (output.Connections.Count > 0)
+                return true;
+        }
+
+        return false;
+    }
+}

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

@@ -29,6 +29,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
     public event Action ConnectionsChanged;
+    public event Action OutputsChanged;
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
@@ -421,9 +422,17 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
     }
 
+
+    protected void RemoveOutputProperty(OutputProperty property)
+    {
+        outputs.Remove(property);
+        OutputsChanged?.Invoke();
+    }
+
     protected void AddOutputProperty(OutputProperty property)
     {
         outputs.Add(property);
+        OutputsChanged?.Invoke();
     }
 
     protected void AddInputProperty(InputProperty property)
@@ -571,7 +580,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return GetOutputProperty(outputProperty);
     }
 
-    public virtual void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    public virtual void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
     {
     }
 

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

@@ -65,7 +65,7 @@ public class NoiseNode : RenderNode
         AngleOffset = CreateInput(nameof(AngleOffset), "ANGLE_OFFSET", 0d);
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface target)
+    protected override void OnPaint(RenderContext context, Canvas target)
     {
         if (Math.Abs(previousScale - Scale.Value) > 0.000001
             || previousSeed != Seed.Value
@@ -109,12 +109,12 @@ public class NoiseNode : RenderNode
         RenderNoise(target);
     }
 
-    private void RenderNoise(DrawingSurface workingSurface)
+    private void RenderNoise(Canvas workingSurface)
     {
-        int saved = workingSurface.Canvas.Save();
-        workingSurface.Canvas.Translate(-(float)Offset.Value.X, -(float)Offset.Value.Y);
-        workingSurface.Canvas.DrawPaint(paint);
-        workingSurface.Canvas.RestoreToCount(saved);
+        int saved = workingSurface.Save();
+        workingSurface.Translate(-(float)Offset.Value.X, -(float)Offset.Value.Y);
+        workingSurface.DrawPaint(paint);
+        workingSurface.RestoreToCount(saved);
     }
 
     public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
@@ -133,7 +133,7 @@ public class NoiseNode : RenderNode
         paint.Shader = shader;
         paint.ColorFilter = grayscaleFilter;
         
-        RenderNoise(renderOn);
+        RenderNoise(renderOn.Canvas);
     }
 
     private Shader SelectShader()

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

@@ -52,7 +52,7 @@ public class OutputNode : Node, IRenderInput
                 PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
 
             texture.DrawingSurface.Canvas.Clear();
-            Input.Value?.Paint(previewCtx, texture.DrawingSurface);
+            Input.Value?.Paint(previewCtx, texture.DrawingSurface.Canvas);
             texture.DrawingSurface.Canvas.RestoreToCount(saved);
         }
     }

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

@@ -3,9 +3,9 @@ using Drawie.Backend.Core.Surfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
-public class Painter(Action<RenderContext, DrawingSurface> paint)
+public class Painter(Action<RenderContext, Canvas> paint)
 {
-    public Action<RenderContext, DrawingSurface> Paint { get; } = paint;
+    public Action<RenderContext, Canvas> Paint { get; } = paint;
 
     public override int GetHashCode()
     {

+ 47 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/PointerInfoNode.cs

@@ -0,0 +1,47 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("PointerInfo")]
+public class PointerInfoNode : Node
+{
+    public OutputProperty<VecD> PositionOnCanvas { get; }
+    public OutputProperty<double> Pressure { get; }
+    public OutputProperty<double> Twist { get; }
+    public OutputProperty<VecD> Tilt { get; }
+    public OutputProperty<VecD> MovementDirection { get; }
+    public OutputProperty<double> Rotation { get; }
+    // TODO: Add velocity
+
+    public PointerInfoNode()
+    {
+        PositionOnCanvas = CreateOutput<VecD>("PositionOnCanvas", "POSITION_ON_CANVAS", new VecD(0, 0));
+        Pressure = CreateOutput<double>("Pressure", "PRESSURE", 1.0);
+        Twist = CreateOutput<double>("Twist", "TWIST", 0.0);
+        Tilt = CreateOutput<VecD>("Tilt", "TILT", new VecD(0, 0));
+        MovementDirection = CreateOutput<VecD>("MovementDirection", "MOVEMENT_DIRECTION", new VecD(0, 0));
+        Rotation = CreateOutput<double>("Rotation", "ROTATION", 0.0);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        if (!context.FullRerender && context.PointerInfo.Equals(default))
+        {
+            return;
+        }
+
+        PositionOnCanvas.Value = context.PointerInfo.PositionOnCanvas;
+        Pressure.Value = context.PointerInfo.Pressure;
+        Twist.Value = context.PointerInfo.Twist;
+        Tilt.Value = context.PointerInfo.Tilt;
+        MovementDirection.Value = context.PointerInfo.MovementDirection;
+        Rotation.Value = context.PointerInfo.Rotation;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new PointerInfoNode();
+    }
+}

+ 17 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -13,6 +13,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 public abstract class RenderNode : Node, IHighDpiRenderNode
 {
+    public const string OutputPropertyName = "Output";
     public RenderOutputProperty Output { get; }
 
     public bool AllowHighDpiRendering { get; set; } = false;
@@ -26,7 +27,7 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
     public RenderNode()
     {
         Painter painter = new Painter(Paint);
-        Output = CreateRenderOutput("Output", "OUTPUT",
+        Output = CreateRenderOutput(OutputPropertyName, "OUTPUT",
             () => painter,
             () => this is IRenderInput renderInput ? renderInput.Background.Value : null);
     }
@@ -44,18 +45,18 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
         lastDocumentSize = context.DocumentSize;
     }
 
-    protected virtual void Paint(RenderContext context, DrawingSurface surface)
+    protected virtual void Paint(RenderContext context, Canvas surface)
     {
-        DrawingSurface target = surface;
+        Canvas target = surface;
         bool useIntermediate = !AllowHighDpiRendering
                                && context.RenderOutputSize is { X: > 0, Y: > 0 }
                                && (surface.DeviceClipBounds.Size != context.RenderOutputSize ||
-                                   (RendersInAbsoluteCoordinates && !surface.Canvas.TotalMatrix.IsIdentity));
+                                   (RendersInAbsoluteCoordinates && !surface.TotalMatrix.IsIdentity));
         if (useIntermediate)
         {
             Texture intermediate =
                 textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
-            target = intermediate.DrawingSurface;
+            target = intermediate.DrawingSurface.Canvas;
         }
 
         OnPaint(context, target);
@@ -64,30 +65,30 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
         {
             if (RendersInAbsoluteCoordinates)
             {
-                surface.Canvas.Save();
-                surface.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
+                surface.Save();
+                surface.Scale((float)context.ChunkResolution.InvertedMultiplier());
             }
 
             if (context.DesiredSamplingOptions != SamplingOptions.Default)
             {
-                using var snapshot = target.Snapshot();
-                surface.Canvas.DrawImage(snapshot, 0, 0, context.DesiredSamplingOptions);
+                using var snapshot = target.Surface.Snapshot();
+                surface.DrawImage(snapshot, 0, 0, context.DesiredSamplingOptions);
             }
             else
             {
-                surface.Canvas.DrawSurface(target, 0, 0);
+                surface.DrawSurface(target.Surface, 0, 0);
             }
 
             if (RendersInAbsoluteCoordinates)
             {
-                surface.Canvas.Restore();
+                surface.Restore();
             }
         }
 
         RenderPreviews(context);
     }
 
-    protected abstract void OnPaint(RenderContext context, DrawingSurface surface);
+    protected abstract void OnPaint(RenderContext context, Canvas surface);
 
     protected void RenderPreviews(RenderContext ctx)
     {
@@ -121,7 +122,7 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
             preview.Texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
             preview.Texture.DrawingSurface.Canvas.Translate((float)-bounds.Value.X, (float)-bounds.Value.Y);
 
-            adjusted.RenderSurface = preview.Texture.DrawingSurface;
+            adjusted.RenderSurface = preview.Texture.DrawingSurface.Canvas;
             RenderPreview(preview.Texture.DrawingSurface, adjusted, preview.ElementToRender);
             preview.Texture.DrawingSurface.Canvas.RestoreToCount(saved);
         }
@@ -141,7 +142,7 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
         string elementToRenderName)
     {
         int saved = renderOn.Canvas.Save();
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
         renderOn.Canvas.RestoreToCount(saved);
     }
 
@@ -150,9 +151,9 @@ public abstract class RenderNode : Node, IHighDpiRenderNode
         return textureCache.RequestTexture(id, size, processingCs, clear);
     }
 
-    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    public override void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
     {
-        base.SerializeAdditionalData(additionalData);
+        base.SerializeAdditionalData(target, additionalData);
         additionalData["AllowHighDpiRendering"] = AllowHighDpiRendering;
     }
 

+ 14 - 14
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -123,11 +123,11 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         //texture.DrawingSurface.Canvas.Scale((float)context.ChunkResolution.Multiplier(), (float)context.ChunkResolution.Multiplier());
 
         var ctx = context.Clone();
-        ctx.RenderSurface = texture.DrawingSurface;
+        ctx.RenderSurface = texture.DrawingSurface.Canvas;
         ctx.RenderOutputSize = finalSize;
         ctx.ChunkResolution = ChunkResolution.Full;
 
-        Background.Value.Paint(ctx, texture.DrawingSurface);
+        Background.Value.Paint(ctx, texture.DrawingSurface.Canvas);
         texture.DrawingSurface.Canvas.RestoreToCount(saved);
 
         var snapshot = texture.DrawingSurface.Snapshot();
@@ -142,15 +142,15 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         return uniforms;
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         if (shader == null || paint == null)
         {
-            surface.Canvas.DrawColor(Colors.Magenta, BlendMode.Src);
+            surface.DrawColor(Colors.Magenta, BlendMode.Src);
             return;
         }
 
-        DrawingSurface targetSurface = surface;
+        Canvas targetSurface = surface;
 
         float width = (float)(context.RenderOutputSize.X);
         float height = (float)(context.RenderOutputSize.Y);
@@ -167,7 +167,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
                     : ColorSpace.Value == ColorSpaceType.Srgb
                         ? Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()
                         : Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear());
-            targetSurface = intermediateSurface.DrawingSurface;
+            targetSurface = intermediateSurface.DrawingSurface.Canvas;
             width = (float)(context.RenderOutputSize.X * context.ChunkResolution.InvertedMultiplier());
             height = (float)(context.RenderOutputSize.Y * context.ChunkResolution.InvertedMultiplier());
             scale = true;
@@ -179,29 +179,29 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
                 if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
                 {
                     targetSurface = RequestTexture(51, context.RenderOutputSize,
-                        Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface;
+                        Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()).DrawingSurface.Canvas;
                 }
                 else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
                 {
                     targetSurface = RequestTexture(51, context.RenderOutputSize,
-                        Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface;
+                        Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear()).DrawingSurface.Canvas;
                 }
             }
         }
 
-        targetSurface.Canvas.DrawRect(0, 0, width, height, paint);
+        targetSurface.DrawRect(0, 0, width, height, paint);
 
         if (targetSurface != surface)
         {
-            int saved = surface.Canvas.Save();
+            int saved = surface.Save();
             if (scale)
             {
-                surface.Canvas.Scale((float)context.ChunkResolution.Multiplier(),
+                surface.Scale((float)context.ChunkResolution.Multiplier(),
                     (float)context.ChunkResolution.Multiplier());
             }
 
-            surface.Canvas.DrawSurface(targetSurface, 0, 0);
-            surface.Canvas.RestoreToCount(saved);
+            surface.DrawSurface(targetSurface.Surface, 0, 0);
+            surface.RestoreToCount(saved);
         }
     }
 
@@ -209,7 +209,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     {
         int saved = renderOn.Canvas.Save();
         renderOn.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
         renderOn.Canvas.RestoreToCount(saved);
     }
 

+ 32 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -11,11 +11,37 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
     public VectorPath Path { get; set; }
-    public override RectD GeometryAABB => Path?.TightBounds ?? RectD.Empty;
+
+    public override RectD GeometryAABB
+    {
+        get
+        {
+            var tightBounds = Path?.TightBounds ?? RectD.Empty;
+            if (tightBounds.Width == 0 || tightBounds.Height == 0)
+            {
+                // If the path is a line or a point, we need to inflate the bounds by half the stroke width
+                double halfStroke = StrokeWidth / 2;
+                tightBounds = new RectD(
+                    tightBounds.X - halfStroke,
+                    tightBounds.Y - halfStroke,
+                    tightBounds.Width + StrokeWidth,
+                    tightBounds.Height + StrokeWidth);
+            }
+
+            return tightBounds;
+        }
+    }
+
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
-    public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(Path.TightBounds).WithMatrix(TransformationMatrix);
+    public override ShapeCorners TransformationCorners
+    {
+        get
+        {
+            var tightCorners = new ShapeCorners(GeometryAABB);
+            return tightCorners.WithMatrix(TransformationMatrix);
+        }
+    }
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
 
@@ -133,8 +159,9 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
     protected bool Equals(PathVectorData other)
     {
-        return base.Equals(other) && Path.Equals(other.Path) && StrokeLineCap == other.StrokeLineCap && StrokeLineJoin == other.StrokeLineJoin
-                && FillType == other.FillType;
+        return base.Equals(other) && Path.Equals(other.Path) && StrokeLineCap == other.StrokeLineCap &&
+               StrokeLineJoin == other.StrokeLineJoin
+               && FillType == other.FillType;
     }
 
     public override bool Equals(object? obj)

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;

+ 53 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/PixelPerfectEllipseNode.cs

@@ -0,0 +1,53 @@
+using ChunkyImageLib.Operations;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("PixelPerfectEllipse")]
+public class PixelPerfectEllipseNode : ShapeNode<PathVectorData>
+{
+    public InputProperty<VecD> Center { get; }
+    public InputProperty<VecI> Size { get; }
+    public InputProperty<Paintable> StrokeColor { get; }
+    public InputProperty<Paintable> FillColor { get; }
+    public InputProperty<double> StrokeWidth { get; }
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
+    private VectorPath cachedPath = new();
+    private VecI cachedSize = VecI.Zero;
+    private VecI lastCenter = VecI.Zero;
+
+    public PixelPerfectEllipseNode()
+    {
+        Center = CreateInput<VecD>("Position", "POSITION", VecI.Zero);
+        Size = CreateInput<VecI>("Size", "SIZE", new VecI(32, 32)).WithRules(
+            v => v.Min(new VecI(0)));
+        StrokeColor = CreateInput<Paintable>("StrokeColor", "STROKE_COLOR", new Color(0, 0, 0, 255));
+        FillColor = CreateInput<Paintable>("FillColor", "FILL_COLOR", new Color(0, 0, 0, 255));
+        StrokeWidth = CreateInput<double>("StrokeWidth", "STROKE_WIDTH", 1);
+    }
+    protected override PathVectorData? GetShapeData(RenderContext context)
+    {
+        if(cachedSize != Size.Value)
+        {
+            cachedSize = Size.Value;
+            cachedPath?.Dispose();
+            cachedPath = EllipseHelper.ConstructEllipseOutline(new RectI(VecI.Zero, Size.Value));
+        }
+
+        cachedPath.Offset((Center.Value - new VecD(cachedSize.X, cachedSize.Y)) - lastCenter);
+
+        return new PathVectorData(cachedPath) { Stroke = StrokeColor.Value, FillPaintable = FillColor.Value, StrokeWidth = (float)StrokeWidth.Value };
+    }
+
+    public override Node CreateCopy()
+    {
+        return new PixelPerfectEllipseNode();
+    }
+}

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

@@ -20,7 +20,7 @@ public class RasterizeShapeNode : RenderNode
         HighDpiRendering = CreateInput<bool>("High DPI Rendering", "HIGH_DPI_RENDERING", true);
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         var shape = Data.Value;
 
@@ -29,7 +29,7 @@ public class RasterizeShapeNode : RenderNode
 
         AllowHighDpiRendering = HighDpiRendering.Value;
 
-        shape.RasterizeTransformed(surface.Canvas);
+        shape.RasterizeTransformed(surface);
     }
 
     public override Node CreateCopy() => new RasterizeShapeNode();

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

@@ -16,7 +16,6 @@ public abstract class ShapeNode<T> : Node where T : ShapeVectorData
         Output = CreateOutput<T>("Output", "OUTPUT", null);
     }
     
-    private static readonly Paint rasterizePreviewPaint = new Paint();
 
     protected override void OnExecute(RenderContext context)
     {

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

@@ -129,25 +129,25 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
     }
 
-    protected override void Paint(RenderContext context, DrawingSurface surface)
+    protected override void Paint(RenderContext context, Canvas surface)
     {
         OnPaint(context, surface);
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface renderTarget)
+    protected override void OnPaint(RenderContext context, Canvas renderTarget)
     {
-        if (Output.Connections.Count > 0)
+        if (Output.Connections.Count > 0 && renderTarget != null)
         {
             RenderForOutput(context, renderTarget, Output);
         }
     }
 
-    private void OnFilterlessPaint(RenderContext context, DrawingSurface renderTarget)
+    private void OnFilterlessPaint(RenderContext context, Canvas renderTarget)
     {
         RenderForOutput(context, renderTarget, FilterlessOutput);
     }
 
-    private void OnRawPaint(RenderContext context, DrawingSurface renderTarget)
+    private void OnRawPaint(RenderContext context, Canvas renderTarget)
     {
         RenderForOutput(context, renderTarget, RawOutput);
     }
@@ -155,7 +155,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetSceneSize(KeyFrameTime frameTime);
 
-    public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output)
+    public void RenderForOutput(RenderContext context, Canvas renderTarget, RenderOutputProperty output)
     {
         if (IsDisposed)
         {
@@ -164,7 +164,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
 
-        int renderSaved = renderTarget.Canvas.Save();
+        int renderSaved = renderTarget.Save();
         VecD scenePos = GetScenePosition(context.FrameTime);
         VecD sceneSize = GetSceneSize(context.FrameTime);
         //renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
@@ -177,7 +177,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
         Render(renderObjectContext);
 
-        renderTarget?.Canvas.RestoreToCount(renderSaved);
+        renderTarget?.RestoreToCount(renderSaved);
     }
 
     private bool IsConnectedToCustomShaderNode(RenderOutputProperty output)
@@ -203,7 +203,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         return false;
     }
 
-    protected SceneObjectRenderContext CreateSceneContext(RenderContext context, DrawingSurface renderTarget,
+    protected SceneObjectRenderContext CreateSceneContext(RenderContext context, Canvas renderTarget,
         RenderOutputProperty output)
     {
         var sceneSize = GetSceneSize(context.FrameTime);
@@ -212,7 +212,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
             context.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize,
             renderTarget == context.RenderSurface,
-            context.ProcessingColorSpace, context.DesiredSamplingOptions, context.Opacity);
+            context.ProcessingColorSpace, context.DesiredSamplingOptions, context.Graph, context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
         renderObjectContext.AffectedArea = context.AffectedArea;
         renderObjectContext.VisibleDocumentRegion = context.VisibleDocumentRegion;
@@ -222,17 +222,17 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
     public abstract void Render(SceneObjectRenderContext sceneContext);
 
-    protected void ApplyMaskIfPresent(DrawingSurface surface, RenderContext context, ChunkResolution renderResolution)
+    protected void ApplyMaskIfPresent(Canvas surface, RenderContext context, ChunkResolution renderResolution)
     {
         if (MaskIsVisible.Value)
         {
             if (CustomMask.Value != null)
             {
-                int layer = surface.Canvas.SaveLayer(maskPaint);
-                surface.Canvas.Scale((float)renderResolution.Multiplier());
+                int layer = surface.SaveLayer(maskPaint);
+                surface.Scale((float)renderResolution.Multiplier());
                 CustomMask.Value.Paint(context, surface);
 
-                surface.Canvas.RestoreToCount(layer);
+                surface.RestoreToCount(layer);
             }
             else
             {
@@ -268,7 +268,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         return (MaskIsVisible.Value && (EmbeddedMask != null || CustomMask.Value != null)) || ClipToPreviousMember;
     }
 
-    protected void DrawClipSource(DrawingSurface drawOnto, IClipSource clipSource, SceneObjectRenderContext context)
+    protected void DrawClipSource(Canvas drawOnto, IClipSource clipSource, SceneObjectRenderContext context)
     {
         blendPaint.Color = Colors.White;
         clipSource.DrawClipSource(context, drawOnto);
@@ -277,9 +277,9 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
     public abstract RectD? GetApproxBounds(KeyFrameTime frameTime);
 
-    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    public override void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
     {
-        base.SerializeAdditionalData(additionalData);
+        base.SerializeAdditionalData(target, additionalData);
         if (EmbeddedMask != null)
         {
             additionalData["embeddedMask"] = EmbeddedMask;
@@ -346,7 +346,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         img.DrawMostUpToDateRegionOn(
             new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
             context.ChunkResolution,
-            renderOn, VecI.Zero, maskPreviewPaint, drawPaintOnEmpty: true);
+            renderOn.Canvas, VecI.Zero, maskPreviewPaint, drawPaintOnEmpty: true);
         renderOn.Canvas.RestoreToCount(saved);
     }
 

+ 13 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs

@@ -8,6 +8,7 @@ public class TextIndexOfNode : Node
     public OutputProperty<int> FirstIndex { get; }
     
     public OutputProperty<int> LastIndex { get; }
+    public OutputProperty<int> Count { get; }
     
     public InputProperty<bool> MatchCase { get; }
     
@@ -19,6 +20,7 @@ public class TextIndexOfNode : Node
     {
         FirstIndex = CreateOutput("FirstIndex", "FIRST_POSITION", -1);
         LastIndex = CreateOutput("LastIndex", "LAST_POSITION", -1);
+        Count = CreateOutput("Count", "COUNT", 0);
         
         MatchCase = CreateInput("MatchCase", "MATCH_CASE", false);
         Text = CreateInput("Text", "TEXT", string.Empty);
@@ -38,12 +40,23 @@ public class TextIndexOfNode : Node
         {
             FirstIndex.Value = -1;
             LastIndex.Value = -1;
+            Count.Value = 0;
             
             return;
         }
 
+        int matchCount = 0;
+        int index = 0;
+
+        while ((index = text.IndexOf(searchText, index, comparisonMode)) != -1)
+        {
+            matchCount++;
+            index += searchText.Length;
+        }
+
         FirstIndex.Value = text.IndexOf(searchText, comparisonMode);
         LastIndex.Value = text.LastIndexOf(searchText, comparisonMode);
+        Count.Value = matchCount;
     }
 
     public override Node CreateCopy() =>

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

@@ -57,12 +57,12 @@ public class TileNode : RenderNode
         paint.Shader = tileShader;
     }
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
         if (paint == null)
             return;
 
-        surface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
+        surface.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
     }
 
     public override Node CreateCopy()

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/EqualsNode.cs

@@ -0,0 +1,28 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Utility;
+
+[NodeInfo("Equals")]
+public class EqualsNode : Node
+{
+    public InputProperty<object> A { get; }
+    public InputProperty<object> B { get; }
+    public OutputProperty<bool> Result { get; }
+
+    public EqualsNode()
+    {
+        A = CreateInput<object>("A", "A", null);
+        B = CreateInput<object>("B", "B", null);
+        Result = CreateOutput<bool>("Result", "RESULT", false);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        Result.Value = Equals(A.Value, B.Value);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new EqualsNode();
+    }
+}

+ 41 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/SwitchNode.cs

@@ -0,0 +1,41 @@
+using Drawie.Backend.Core.Shaders.Generation;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Utility;
+
+[NodeInfo("Switch")]
+public class SwitchNode : Node
+{
+    public InputProperty<bool> Condition { get; }
+    public InputProperty<object> InputTrue { get; }
+    public InputProperty<object> InputFalse { get; }
+
+    public OutputProperty<object> Output { get; }
+
+    public SwitchNode()
+    {
+        Condition = CreateInput<bool>("Condition", "CONDITION", false);
+        InputTrue = CreateInput<object>("InputTrue", "ON_TRUE", null);
+        InputFalse = CreateInput<object>("InputFalse", "ON_FALSE", null);
+        Output = CreateOutput<object>("Output", "RESULT", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        Output.Value = Condition.Value ? InputTrue.Value : InputFalse.Value;
+        if(Output.Value is Delegate del)
+        {
+            Output.Value = del.DynamicInvoke(FuncContext.NoContext);
+            if(Output.Value is ShaderExpressionVariable expr)
+            {
+                Output.Value = expr.GetConstant();
+            }
+        }
+    }
+
+    public override Node CreateCopy()
+    {
+        return new SwitchNode();
+    }
+}

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

@@ -69,7 +69,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         Matrix.Value = TransformationMatrix;
     }
 
-    protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected override void DrawWithoutFilters(SceneObjectRenderContext ctx, Canvas workingSurface,
         Paint paint)
     {
         if (RenderableShapeData == null)
@@ -77,17 +77,17 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
             return;
         }
 
-        Rasterize(workingSurface, paint);
+        Rasterize(workingSurface, paint, ctx.FrameTime.Frame);
     }
 
-    protected override void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface, Paint paint)
+    protected override void DrawWithFilters(SceneObjectRenderContext ctx, Canvas workingSurface, Paint paint)
     {
         if (RenderableShapeData == null)
         {
             return;
         }
 
-        Rasterize(workingSurface, paint);
+        Rasterize(workingSurface, paint, ctx.FrameTime.Frame);
     }
 
     protected override bool ShouldRenderPreview(string elementToRenderName)
@@ -136,7 +136,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
             return;
         }
 
-        Rasterize(renderOn, paint);
+        Rasterize(renderOn.Canvas, paint, context.FrameTime.Frame);
     }
 
     public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
@@ -154,9 +154,9 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         return GetTightBounds(frameTime);
     }
 
-    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    public override void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
     {
-        base.SerializeAdditionalData(additionalData);
+        base.SerializeAdditionalData(target, additionalData);
         additionalData["ShapeData"] = EmbeddedShapeData;
     }
 
@@ -196,24 +196,22 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         return RenderableShapeData?.TransformationCorners ?? new ShapeCorners();
     }
 
-    public void Rasterize(DrawingSurface surface, Paint paint)
+    public void Rasterize(Canvas surface, Paint paint, int frame)
     {
         int layer;
         // TODO: This can be further optimized by passing opacity, blend mode and filters directly to the rasterization method
-        if (paint != null && (paint.Color.A < 255 || paint.ColorFilter != null || paint.ImageFilter != null ||
-                              paint.Shader != null ||
-                              paint.BlendMode != Drawie.Backend.Core.Surfaces.BlendMode.SrcOver))
+        if (paint is { IsOpaqueStandardNonBlendingPaint: false })
         {
-            layer = surface.Canvas.SaveLayer(paint);
+            layer = surface.SaveLayer(paint);
         }
         else
         {
-            layer = surface.Canvas.Save();
+            layer = surface.Save();
         }
 
-        RenderableShapeData?.RasterizeTransformed(surface.Canvas);
+        RenderableShapeData?.RasterizeTransformed(surface);
 
-        surface.Canvas.RestoreToCount(layer);
+        surface.RestoreToCount(layer);
     }
 
     public override Node CreateCopy()

+ 36 - 26
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs

@@ -11,14 +11,12 @@ public class CustomOutputNode : Node, IRenderInput
     public const string OutputNamePropertyName = "OutputName";
     public const string IsDefaultExportPropertyName = "IsDefaultExport";
     public const string SizePropertyName = "Size";
+    public const string FullViewportRenderPropertyName = "FullViewportRender";
     public RenderInputProperty Input { get; }
     public InputProperty<string> OutputName { get; }
     public InputProperty<bool> IsDefaultExport { get; }
     public InputProperty<VecI> Size { get; }
-
-    private VecI? lastDocumentSize;
-
-    private TextureCache textureCache = new TextureCache();
+    public InputProperty<bool> FullViewportRender { get; }
 
     public CustomOutputNode()
     {
@@ -28,6 +26,7 @@ public class CustomOutputNode : Node, IRenderInput
         OutputName = CreateInput(OutputNamePropertyName, "OUTPUT_NAME", "");
         IsDefaultExport = CreateInput(IsDefaultExportPropertyName, "IS_DEFAULT_EXPORT", false);
         Size = CreateInput(SizePropertyName, "SIZE", VecI.Zero);
+        FullViewportRender = CreateInput(FullViewportRenderPropertyName, "FULL_VIEWPORT_RENDER", false);
     }
 
     public override Node CreateCopy()
@@ -43,47 +42,58 @@ public class CustomOutputNode : Node, IRenderInput
                 ? context.RenderOutputSize
                 : (VecI)(Size.Value * context.ChunkResolution.Multiplier());
 
-            lastDocumentSize = targetSize;
+            Canvas targetSurface = context.RenderSurface;
 
-            DrawingSurface targetSurface = context.RenderSurface;
-
-            int saved = targetSurface.Canvas.Save();
+            int saved = targetSurface.Save();
 
             Input.Value?.Paint(context, targetSurface);
 
-            targetSurface.Canvas.RestoreToCount(saved);
+            targetSurface.RestoreToCount(saved);
 
             if (targetSurface != context.RenderSurface)
             {
-                context.RenderSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+                context.RenderSurface.DrawSurface(targetSurface.Surface, 0, 0);
             }
+
+            RenderPreviews(context);
         }
     }
 
     RenderInputProperty IRenderInput.Background => Input;
 
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    protected void RenderPreviews(RenderContext ctx)
     {
-        if (lastDocumentSize == null)
+        var previewToRender = ctx.GetPreviewTexturesForNode(Id);
+        if (previewToRender == null || previewToRender.Count == 0)
+            return;
+
+        foreach (var preview in previewToRender)
         {
-            return null;
-        }
+            if (preview.Texture == null)
+                continue;
 
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
-    }
+            int saved = preview.Texture.DrawingSurface.Canvas.Save();
+            preview.Texture.DrawingSurface.Canvas.Clear();
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Input.Value == null)
-        {
-            return false;
-        }
+            var bounds = new RectD(0, 0, ctx.RenderOutputSize.X, ctx.RenderOutputSize.Y);
 
-        int saved = renderOn.Canvas.Save();
-        Input.Value.Paint(context, renderOn);
+            VecD scaling = PreviewUtility.CalculateUniformScaling(bounds.Size, preview.Texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(bounds.Size, preview.Texture.Size, scaling);
+            RenderContext adjusted =
+                PreviewUtility.CreatePreviewContext(ctx, scaling, bounds.Size, preview.Texture.Size);
 
-        renderOn.Canvas.RestoreToCount(saved);
+            preview.Texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            preview.Texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            preview.Texture.DrawingSurface.Canvas.Translate((float)-bounds.X, (float)-bounds.Y);
 
-        return true;
+            adjusted.RenderSurface = preview.Texture.DrawingSurface.Canvas;
+            RenderPreview(preview.Texture.DrawingSurface.Canvas, adjusted);
+            preview.Texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
+    }
+
+    protected virtual void RenderPreview(Canvas surface, RenderContext context)
+    {
+        Input.Value?.Paint(context, surface);
     }
 }

+ 26 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ExposeValueNode.cs

@@ -0,0 +1,26 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
+
+[NodeInfo("ExposeValue")]
+public class ExposeValueNode : Node
+{
+    public InputProperty<string> Name { get; }
+    public InputProperty<object?> Value { get; }
+
+    public ExposeValueNode()
+    {
+        Name = CreateInput<string>("Name", "NAME", "");
+        Value = CreateInput<object?>("Input", "INPUT", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        // no op
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ExposeValueNode();
+    }
+}

+ 38 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ViewportInfoNode.cs

@@ -0,0 +1,38 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
+
+[NodeInfo("ViewportInfo")]
+public class ViewportInfoNode : Node
+{
+    public OutputProperty<Matrix3X3> Transform { get; }
+    public OutputProperty<VecD> PanPosition { get; }
+    public OutputProperty<double> Zoom { get; }
+    public OutputProperty<bool> FlipX { get; }
+    public OutputProperty<bool> FlipY { get; }
+
+    public ViewportInfoNode()
+    {
+        Transform = CreateOutput<Matrix3X3>("Transform", "TRANSFORM", Matrix3X3.Identity);
+        PanPosition = CreateOutput<VecD>("PanPosition", "PAN_POSITION", VecD.Zero);
+        Zoom = CreateOutput<double>("Zoom", "ZOOM", 1.0);
+        FlipX = CreateOutput<bool>("FlipX", "FLIP_X", false);
+        FlipY = CreateOutput<bool>("FlipY", "FLIP_Y", false);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        Transform.Value = context.ViewportData.Transform;
+        PanPosition.Value = context.ViewportData.Translation;
+        Zoom.Value = context.ViewportData.Zoom;
+        FlipX.Value = context.ViewportData.FlipX;
+        FlipY.Value = context.ViewportData.FlipY;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ViewportInfoNode();
+    }
+}

+ 6 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IInputDependentOutputs.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface IInputDependentOutputs
+{
+    public void UpdateOutputs();
+}

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

@@ -1,9 +1,10 @@
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 public interface IRasterizable
 {
-    public void Rasterize(DrawingSurface surface, Paint paint);
+    public void Rasterize(Canvas surface, Paint paint, int atFrame);
 }

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

@@ -8,4 +8,5 @@ public interface IReadOnlyAnimationData
     public double OnionOpacity { get; }
     public int DefaultEndFrame { get; }
     public bool TryFindKeyFrame<T>(Guid id, out T keyFrame) where T : IReadOnlyKeyFrame;
+    public IReadOnlyAnimationData Clone();
 }

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

@@ -8,7 +8,7 @@ using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
-public interface IReadOnlyDocument : IDisposable
+public interface IReadOnlyDocument : IDisposable, ICloneable
 {    
     public Guid DocumentId { get; }
     /// <summary>
@@ -47,7 +47,7 @@ public interface IReadOnlyDocument : IDisposable
     /// The position of the vertical symmetry axis (Mirrors left and right)
     /// </summary>
     double VerticalSymmetryAxisX { get; }
-    
+
     /// <summary>
     /// Performs the specified action on each readonly member of the document
     /// </summary>
@@ -102,8 +102,12 @@ public interface IReadOnlyDocument : IDisposable
     IReadOnlyList<IReadOnlyStructureNode> FindMemberPath(Guid guid);
     IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public DocumentRenderer Renderer { get; }
+    public IReadOnlyBlackboard Blackboard { get; }
     public ColorSpace ProcessingColorSpace { get; }
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
     public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
     public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode;
+    public ICrossDocumentPipe<IReadOnlyNodeGraph> CreateGraphPipe();
+    public IReadOnlyDocument Clone(bool preserveDocumentId = false);
+    public IReadOnlyStructureNode[] GetStructureTreeInOrder();
 }

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

@@ -0,0 +1,9 @@
+using Drawie.Backend.Core.Surfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface IVariableSampling
+{
+    public InputProperty<bool> BilinearSampling { get; }
+}

+ 0 - 186
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -1,186 +0,0 @@
-using ChunkyImageLib.Operations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using Drawie.Backend.Core.ColorsImpl;
-using Drawie.Backend.Core.Numerics;
-using Drawie.Backend.Core.Surfaces;
-using Drawie.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-
-internal class ChangeBrightness_UpdateableChange : UpdateableChange
-{
-    private readonly Guid layerGuid;
-    private readonly float correctionFactor;
-    private readonly int strokeWidth;
-    private readonly List<VecI> positions = new();
-    private readonly bool repeat;
-    private int frame;
-    private int lastAppliedPointIndex = -1;
-    private bool squareBrush;
-
-    private List<VecI> ellipseLines;
-    private RectI squareRect;
-
-    private CommittedChunkStorage? savedChunks;
-
-    [GenerateUpdateableChangeActions]
-    public ChangeBrightness_UpdateableChange(Guid layerGuid, VecI pos, float correctionFactor, int strokeWidth,
-        bool square,
-        bool repeat, int frame)
-    {
-        this.layerGuid = layerGuid;
-        this.correctionFactor = correctionFactor;
-        this.strokeWidth = strokeWidth;
-        this.repeat = repeat;
-        this.frame = frame;
-        this.squareBrush = square;
-        positions.Add(pos);
-
-        squareRect = new RectI(0, 0, strokeWidth, strokeWidth);
-        if (!square)
-        {
-            ellipseLines =
-                EllipseHelper.SplitEllipseIntoLines(
-                    (EllipseHelper.GenerateEllipseFromRect(squareRect, 0)));
-        }
-    }
-
-    [UpdateChangeMethod]
-    public void Update(VecI pos)
-    {
-        if (positions.Count > 0)
-        {
-            var bresenham = BresenhamLineHelper.GetBresenhamLine(positions[^1], pos);
-            positions.AddRange(bresenham);
-        }
-    }
-
-    public override bool InitializeAndValidate(Document target)
-    {
-        if (!DrawingChangeHelper.IsValidForDrawing(target, layerGuid, false))
-            return false;
-        ImageLayerNode node = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-        var layerImage = node.GetLayerImageAtFrame(frame);
-        DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
-        return true;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        ImageLayerNode node = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-
-        var layerImage = node.GetLayerImageAtFrame(frame);
-        int queueLength = layerImage.QueueLength;
-
-        for (int i = Math.Max(lastAppliedPointIndex, 0); i < positions.Count; i++)
-        {
-            VecI pos = positions[i];
-            if (squareBrush)
-            {
-                ChangeBrightness(squareRect, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                    layerImage);
-            }
-            else
-            {
-                ChangeBrightness(ellipseLines, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                    layerImage);
-            }
-        }
-
-        var affected = layerImage.FindAffectedArea(queueLength);
-
-        lastAppliedPointIndex = positions.Count - 1;
-
-        return new LayerImageArea_ChangeInfo(layerGuid, affected);
-    }
-
-    private static void ChangeBrightness(
-        List<VecI> circleLines, VecI offset, float correctionFactor, bool repeat,
-        ChunkyImage layerImage)
-    {
-        for (var i = 0; i < circleLines.Count - 1; i++)
-        {
-            VecI left = circleLines[i];
-            VecI right = circleLines[i + 1];
-            int y = left.Y;
-
-            for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
-            {
-                layerImage.EnqueueDrawPixel(
-                    pos + offset,
-                    (commitedColor, upToDateColor) =>
-                    {
-                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor,
-                            correctionFactor);
-                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
-                    },
-                    BlendMode.Src);
-            }
-        }
-    }
-
-    private static void ChangeBrightness(
-        RectI square, VecI offset, float correctionFactor, bool repeat,
-        ChunkyImage layerImage)
-    {
-        for (int i = square.X; i < square.X + square.Width; i++)
-        {
-            for (int j = square.Y; j < square.Y + square.Height; j++)
-            {
-                layerImage.EnqueueDrawPixel(
-                    new VecI(i, j) + offset,
-                    (commitedColor, upToDateColor) =>
-                    {
-                        Color newColor = ColorHelper.ChangeColorBrightness(repeat ? upToDateColor : commitedColor,
-                            correctionFactor);
-                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
-                    },
-                    BlendMode.Src);
-            }
-        }
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
-        out bool ignoreInUndo)
-    {
-        var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-        ignoreInUndo = false;
-
-        if (savedChunks is not null)
-            throw new InvalidOperationException("Trying to apply while there are saved chunks");
-
-        var layerImage = layer.GetLayerImageAtFrame(frame);
-
-        if (!firstApply)
-        {
-            DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, layerImage, layerGuid, false);
-            foreach (VecI pos in positions)
-            {
-                if (squareBrush)
-                {
-                    ChangeBrightness(squareRect, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                        layerImage);
-                }
-                else
-                {
-                    ChangeBrightness(ellipseLines, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat,
-                        layerImage);
-                }
-            }
-        }
-
-        var affArea = layerImage.FindAffectedArea();
-        savedChunks = new CommittedChunkStorage(layerImage, affArea.Chunks);
-        layerImage.CommitChanges();
-        if (firstApply)
-            return new None();
-        return new LayerImageArea_ChangeInfo(layerGuid, affArea);
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
-    {
-        var affected =
-            DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, layerGuid, false, frame, ref savedChunks);
-        return new LayerImageArea_ChangeInfo(layerGuid, affected);
-    }
-}

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

@@ -75,7 +75,7 @@ internal class FloodFillChunkCache : IDisposable
             return new EmptyChunk();
         Chunk chunkOnImage = Chunk.Create(processingColorSpace, ChunkResolution.Full);
 
-        if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunkOnImage.Surface.DrawingSurface, VecI.Zero, ReplacingPaint))
+        if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunkOnImage.Surface.DrawingSurface.Canvas, VecI.Zero, ReplacingPaint))
         {
             chunkOnImage.Dispose();
             acquiredChunks[pos] = new EmptyChunk();

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff