浏览代码

Merge branch 'master' into pr/1273

Krzysztof Krysiński 1 周之前
父节点
当前提交
39a7133083
共有 100 个文件被更改,包括 3153 次插入739 次删除
  1. 1 1
      samples/Directory.Build.props
  2. 3 3
      src/ChunkyImageLib/Chunk.cs
  3. 43 22
      src/ChunkyImageLib/ChunkyImage.cs
  4. 17 17
      src/ChunkyImageLib/ChunkyImageEx.cs
  5. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  6. 3 3
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  7. 1 1
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  8. 4 4
      src/ChunkyImageLib/Operations/BresenhamLineHelper.cs
  9. 5 5
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  10. 9 3
      src/ChunkyImageLib/Operations/ImageOperation.cs
  11. 24 0
      src/ChunkyImageLib/Operations/LineHelper.cs
  12. 53 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  13. 3 24
      src/ChunkyImageLib/Operations/PixelOperation.cs
  14. 6 1
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  15. 1 1
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  16. 3 1
      src/ChunkyImageLib/Operations/TextureOperation.cs
  17. 1 1
      src/ColorPicker
  18. 1 1
      src/Directory.Build.props
  19. 1 1
      src/Drawie
  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. 30 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. 17 17
      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. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  81. 53 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/PixelPerfectEllipseNode.cs
  82. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  83. 0 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  84. 18 18
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  85. 13 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs
  86. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  87. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/EqualsNode.cs
  88. 41 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Utility/SwitchNode.cs
  89. 13 15
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  90. 36 26
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/CustomOutputNode.cs
  91. 26 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ExposeValueNode.cs
  92. 38 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Workspace/ViewportInfoNode.cs
  93. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IInputDependentOutputs.cs
  94. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IRasterizable.cs
  95. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyAnimationData.cs
  96. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  97. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IVariableSampling.cs
  98. 0 186
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  99. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkCache.cs
  100. 78 156
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

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

+ 43 - 22
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;
@@ -271,10 +272,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
             }
 
-            /*if (lastLatestBoundsCacheHash == GetCacheHash())
+            if (lastLatestBoundsCacheHash == GetCacheHash())
             {
                 return cachedPreciseLatestBounds;
-            }*/
+            }
 
             var chunkSize = suggestedResolution.PixelSize();
             var multiplier = suggestedResolution.Multiplier();
@@ -482,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)
@@ -540,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)
     {
@@ -636,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)
@@ -921,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)
     {
@@ -976,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)
     {
@@ -1311,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])
             {
@@ -1319,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();
                 }
             }
 
@@ -1433,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
@@ -1482,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;
         }
 
@@ -1610,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;

+ 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/Drawie

@@ -1 +1 @@
-Subproject commit 64373b7e3e5131a31e8a8d1d394267bd3f058f75
+Subproject commit bc83f2b962f7e2ff4140bda8dd421aa2309f2488

+ 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

@@ -72,13 +72,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;
 
         float chunkMultiplier = (float)context.ChunkResolution.Multiplier();
@@ -87,21 +88,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();
+    }
 }

+ 30 - 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();
     }
 
@@ -313,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)
         {
@@ -329,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()

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

@@ -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);
     }
 

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

+ 78 - 156
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -1,4 +1,7 @@
-using ChunkyImageLib.Operations;
+using System.Diagnostics;
+using System.Reflection;
+using ChunkyImageLib.Operations;
+using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
@@ -6,211 +9,129 @@ using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Brushes;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Brushes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Rendering.ContextData;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal class LineBasedPen_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
-    private readonly Color color;
     private float strokeWidth;
-    private readonly bool erasing;
     private readonly bool drawOnMask;
     private readonly bool antiAliasing;
-    private bool squareBrush;
-    private float hardness;
-    private float spacing = 1;
-    private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
-    private Paintable? finalPaintable;
+    private BrushData brushData;
+    private BrushEngine engine = new BrushEngine();
 
     private CommittedChunkStorage? storedChunks;
-    private readonly List<VecI> points = new();
+    private readonly List<RecordedPoint> points = new();
+
+    private int cachedCount = -1;
     private int frame;
-    private VecF lastPos;
-    private int lastAppliedPointIndex = -1;
+    private BrushOutputNode? brushOutputNode;
+    private PointerInfo pointerInfo;
+    private KeyboardInfo keyboardInfo;
+    private EditorData editorData;
 
     [GenerateUpdateableChangeActions]
-    public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
+    public LineBasedPen_UpdateableChange(Guid memberGuid, VecD pos, float strokeWidth,
         bool antiAliasing,
-        float hardness,
-        float spacing,
-        bool squareBrush,
-        bool drawOnMask, int frame)
+        BrushData brushData,
+        bool drawOnMask, int frame, PointerInfo pointerInfo, KeyboardInfo keyboardInfo, EditorData editorData)
     {
         this.memberGuid = memberGuid;
-        this.color = color;
         this.strokeWidth = strokeWidth;
-        this.erasing = erasing;
         this.antiAliasing = antiAliasing;
         this.drawOnMask = drawOnMask;
-        this.hardness = hardness;
-        this.spacing = spacing;
-        this.squareBrush = squareBrush;
-        points.Add(pos);
+        this.brushData = brushData;
+        points.Add(new RecordedPoint(pos, pointerInfo, keyboardInfo, editorData));
         this.frame = frame;
-
-        srcPaint.Shader?.Dispose();
-        srcPaint.Shader = null;
-
-        if (this.antiAliasing && !erasing)
-        {
-            srcPaint.BlendMode = BlendMode.SrcOver;
-        }
-        else if (erasing)
-        {
-            srcPaint.BlendMode = BlendMode.DstOut;
-            if (this.color.A == 0)
-            {
-                this.color = color.WithAlpha(255);
-            }
-        }
+        this.pointerInfo = pointerInfo;
+        this.keyboardInfo = keyboardInfo;
+        this.editorData = editorData;
     }
 
     [UpdateChangeMethod]
-    public void Update(VecI pos, float strokeWidth)
+    public void Update(VecD pos, float strokeWidth, PointerInfo pointerInfo, KeyboardInfo keyboardInfo,
+        EditorData editorData, BrushData brushData)
     {
-        if (points.Count > 0)
-        {
-            var bresenham = BresenhamLineHelper.GetBresenhamLine(points[^1], pos);
-            points.AddRange(bresenham);
-        }
-
+        points.Add(new RecordedPoint(pos, pointerInfo, keyboardInfo, editorData));
         this.strokeWidth = strokeWidth;
+        this.pointerInfo = pointerInfo;
+        this.keyboardInfo = keyboardInfo;
+        this.editorData = editorData;
+        this.brushData = brushData;
+        UpdateBrushData();
     }
 
     public override bool InitializeAndValidate(Document target)
     {
         if (!DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask))
             return false;
-        if (strokeWidth < 1)
+        if (strokeWidth < 0.1)
             return false;
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
-        if (!erasing)
-            image.SetBlendMode(BlendMode.SrcOver);
-        DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
-        srcPaint.IsAntiAliased = antiAliasing;
-        return true;
-    }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
+        DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
-        int opCount = image.QueueLength;
+        brushOutputNode = brushData.BrushGraph?.AllNodes.FirstOrDefault(x => x is BrushOutputNode) as BrushOutputNode;
+        UpdateBrushData();
 
-        float spacingPixels = strokeWidth * spacing;
+        return brushOutputNode != null;
+    }
 
-        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+    private void UpdateBrushData()
+    {
+        if (brushOutputNode != null)
         {
-            var point = points[i];
-            if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
-                continue;
-
-            lastPos = point;
-            var rect = new RectI(point - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            finalPaintable = color;
-
-            if (!squareBrush)
+            brushData = new BrushData(brushData.BrushGraph, brushData.TargetBrushNodeId)
             {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient((VecD)point);
-                }
-
-                image.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing, srcPaint);
-            }
-            else
-            {
-                BlendMode blendMode = srcPaint.BlendMode;
-                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
-                    blendMode);
-                image.EnqueueDrawRectangle(shapeData);
-            }
+                StrokeWidth = strokeWidth, AntiAliasing = antiAliasing, ForcePressure = brushData.ForcePressure
+            };
         }
-
-        lastAppliedPointIndex = points.Count - 1;
-
-        var affChunks = image.FindAffectedArea(opCount);
-
-        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
     }
 
-    private void FastforwardEnqueueDrawLines(ChunkyImage targetImage)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
-        if (points.Count == 1)
-        {
-            var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            finalPaintable = color;
-
-            if (!squareBrush)
-            {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient(points[0]);
-                }
-
-                targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing,
-                    srcPaint);
-            }
-            else
-            {
-                BlendMode blendMode = srcPaint.BlendMode;
-                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
-                    blendMode);
-                targetImage.EnqueueDrawRectangle(shapeData);
-            }
-
-            return;
-        }
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
 
-        VecF lastPos = points[0];
+        int opCount = image.QueueLength;
 
-        float spacingInPixels = strokeWidth * this.spacing;
+        brushData.AntiAliasing = antiAliasing;
+        brushData.StrokeWidth = strokeWidth;
 
-        for (int i = 0; i < points.Count; i++)
-        {
-            if (i > 0 && VecF.Distance(lastPos, points[i]) < spacingInPixels)
-                continue;
+        // TODO: Sampling options?
+        engine.ExecuteBrush(image, brushData, points, frame, target.ProcessingColorSpace, SamplingOptions.Default);
 
-            lastPos = points[i];
-            var rect = new RectI(points[i] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            finalPaintable = color;
+        var affChunks = image.FindAffectedArea(opCount);
 
-            if (!squareBrush)
-            {
-                if (antiAliasing)
-                {
-                    finalPaintable = ApplySoftnessGradient(points[i]);
-                }
+        var changeInfo = DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
 
-                targetImage.EnqueueDrawEllipse((RectD)rect, finalPaintable, finalPaintable, 0, 0, antiAliasing,
-                    srcPaint);
-            }
-            else
-            {
-                BlendMode blendMode = srcPaint.BlendMode;
-                ShapeData shapeData = new ShapeData(rect.Center, rect.Size, 0, 0, 0, finalPaintable, finalPaintable,
-                    blendMode);
-                targetImage.EnqueueDrawRectangle(shapeData);
-            }
-        }
+        return changeInfo;
     }
 
-    private Paintable? ApplySoftnessGradient(VecD pos)
+    private void FastforwardEnqueueDrawLines(ChunkyImage targetImage, KeyFrameTime frameTime)
     {
-        srcPaint.Paintable?.Dispose();
-        if (hardness >= 1)
+        brushData.AntiAliasing = antiAliasing;
+        brushData.StrokeWidth = strokeWidth;
+        engine.ResetState();
+
+        if (points.Count == 1)
         {
-            return new ColorPaintable(color);
+            engine.ExecuteBrush(targetImage, brushData, points[0].Position, frameTime, targetImage.ProcessingColorSpace,
+                SamplingOptions.Default, pointerInfo, keyboardInfo, editorData);
+
+            return;
         }
 
-        float radius = strokeWidth / 2f;
-        radius = MathF.Max(1, radius);
-        return new RadialGradientPaintable(pos, radius,
-        [
-            new GradientStop(color, Math.Max(hardness - 0.05f, 0)),
-            new GradientStop(color.WithAlpha(0), 0.95f)
-        ]) { AbsoluteValues = true };
+        engine.ExecuteBrush(targetImage, brushData, points, frameTime, targetImage.ProcessingColorSpace,
+            SamplingOptions.Default);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
@@ -227,20 +148,21 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
+            var change = DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask).AsT1;
+            return OneOf<None, IChangeInfo, List<IChangeInfo>>.FromT1(change);
         }
         else
         {
-            if (!erasing)
-                image.SetBlendMode(BlendMode.SrcOver);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
-            FastforwardEnqueueDrawLines(image);
+            FastforwardEnqueueDrawLines(image, frame);
             var affArea = image.FindAffectedArea();
             storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
+            IChangeInfo info = DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask).AsT1;
+
+            return OneOf<None, IChangeInfo, List<IChangeInfo>>.FromT1(info);
         }
     }
 
@@ -255,6 +177,6 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     public override void Dispose()
     {
         storedChunks?.Dispose();
-        srcPaint.Dispose();
+        engine?.Dispose();
     }
 }

部分文件因为文件数量过多而无法显示