Browse Source

Merge branch 'brush-engine' into development

Krzysztof Krysiński 2 weeks ago
parent
commit
93777bc15a
100 changed files with 3446 additions and 784 deletions
  1. 1 1
      README.md
  2. BIN
      assets/flatpak/icon-512.png
  3. 14 0
      assets/flatpak/net.pixieditor.PixiEditor-mime.xml
  4. 13 0
      assets/flatpak/net.pixieditor.PixiEditor.desktop
  5. 87 0
      assets/flatpak/net.pixieditor.PixiEditor.metainfo.xml
  6. BIN
      assets/flatpak/screenshots/anim.png
  7. BIN
      assets/flatpak/screenshots/graph.png
  8. BIN
      assets/flatpak/screenshots/palettes.png
  9. BIN
      assets/flatpak/screenshots/vector.png
  10. 22 11
      src/ChunkyImageLib/Chunk.cs
  11. 123 25
      src/ChunkyImageLib/ChunkyImage.cs
  12. 94 15
      src/ChunkyImageLib/ChunkyImageEx.cs
  13. 2 2
      src/ChunkyImageLib/CommittedChunkStorage.cs
  14. 3 2
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  15. 1 1
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  16. 4 4
      src/ChunkyImageLib/Operations/BresenhamLineHelper.cs
  17. 5 5
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  18. 9 3
      src/ChunkyImageLib/Operations/ImageOperation.cs
  19. 24 0
      src/ChunkyImageLib/Operations/LineHelper.cs
  20. 21 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  21. 3 24
      src/ChunkyImageLib/Operations/PixelOperation.cs
  22. 6 1
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  23. 1 1
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  24. 3 1
      src/ChunkyImageLib/Operations/TextureOperation.cs
  25. 1 1
      src/ColorPicker
  26. 1 1
      src/Directory.Build.props
  27. 1 1
      src/Drawie
  28. 1 1
      src/PixiDocks
  29. 8 14
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  30. 1 1
      src/PixiEditor.Api.CGlueMSBuild/PixiEditor.Api.CGlueMSBuild.csproj
  31. 6 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableExposed_ChangeInfo.cs
  32. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariableRemoved_ChangeInfo.cs
  33. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/BlackboardVariable_ChangeInfo.cs
  34. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/Blackboard/RenameBlackboardVariable_ChangeInfo.cs
  35. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  36. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  37. 4 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/NestedDocumentLink_ChangeInfo.cs
  38. 17 0
      src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs
  39. 19 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushData.cs
  40. 521 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/BrushEngine.cs
  41. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/IBrush.cs
  42. 20 0
      src/PixiEditor.ChangeableDocument/Changeables/Brushes/RecordedPoint.cs
  43. 49 6
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  44. 16 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentGraphPipe.cs
  45. 22 0
      src/PixiEditor.ChangeableDocument/Changeables/DocumentReference.cs
  46. 119 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Blackboard.cs
  47. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/BrushRenderContext.cs
  48. 39 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Context/FuncContext.cs
  49. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Filter.cs
  50. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/IReadOnlyBlackboard.cs
  51. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  52. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IClipSource.cs
  53. 0 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IPreviewRenderable.cs
  54. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyLayerNode.cs
  55. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  56. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNodeGraph.cs
  57. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  58. 19 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/SceneObjectRenderContext.cs
  59. 166 26
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  60. 5 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Animable/TimeNode.cs
  61. 54 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/BlackboardVariableValueNode.cs
  62. 283 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/BrushOutputNode.cs
  63. 9 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/IBrushSampleTextureNode.cs
  64. 50 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Brushes/StrokeInfoNode.cs
  65. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CacheTriggerFlags.cs
  66. 22 22
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/CombineChannelsNode.cs
  67. 21 20
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs
  68. 38 42
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  69. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Editor/EditorInfoNode.cs
  70. 10 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/OutlineNode.cs
  71. 6 17
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  72. 29 23
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  73. 13 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ColorAdjustmentsFilterNode.cs
  74. 50 32
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  75. 144 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/GradientNode.cs
  76. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Image/MaskNode.cs
  77. 78 96
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  78. 33 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/KeyboardInfoNode.cs
  79. 49 30
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/LayerNode.cs
  80. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  81. 35 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  82. 11 38
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  83. 34 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  84. 10 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  85. 479 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NestedDocumentNode.cs
  86. 22 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  87. 10 16
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/NoiseNode.cs
  88. 19 24
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/OutputNode.cs
  89. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Painter.cs
  90. 47 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/PointerInfoNode.cs
  91. 72 26
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  92. 18 23
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs
  93. 32 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  94. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  95. 53 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/PixelPerfectEllipseNode.cs
  96. 10 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  97. 0 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  98. 76 92
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  99. 13 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs
  100. 2 13
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs

+ 1 - 1
README.md

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

BIN
assets/flatpak/icon-512.png


+ 14 - 0
assets/flatpak/net.pixieditor.PixiEditor-mime.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+
+  <!-- Native Pixi format -->
+  <mime-type type="application/x-pixi">
+    <comment>Pixi Project File</comment>
+    <generic-icon name="application-x-pixi"/>
+    <glob pattern="*.pixi"/>
+  </mime-type>
+</mime-info>
+

+ 13 - 0
assets/flatpak/net.pixieditor.PixiEditor.desktop

@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=PixiEditor
+Comment=PixiEditor is all-in-one solution for 2D image editing.
+Icon=net.pixieditor.PixiEditor
+Exec=pixieditor.sh %u
+StartupWMClass=pixieditor
+Terminal=false
+Type=Application
+Categories=Graphics;2DGraphics;RasterGraphics;VectorGraphics
+MimeType=application/x-pixi;image/jpeg;image/png;image/gif;image/bmp;image/webp;image/svg+xml;font/otf;font/ttf;x-scheme-handler/lospec-palette;
+GenericName=2D Editor
+SingleMainWindow=true
+Keywords=editor;image;2d;graphics;design;vector;raster;

+ 87 - 0
assets/flatpak/net.pixieditor.PixiEditor.metainfo.xml

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2025 Krzysztof Krysiński -->
+<component type="desktop-application">
+  <id>net.pixieditor.PixiEditor</id>
+  
+  <name>PixiEditor</name>
+  <summary>Universal node-based 2D editor</summary>
+  
+  <metadata_license>CC-BY-SA-4.0</metadata_license>
+  <project_license>LGPL-3.0-or-later</project_license>
+
+  <developer id="net.pixieditor">
+    <name>PixiEditor</name>
+  </developer>
+
+  <requires>
+    <control>keyboard</control>
+    <control>pointing</control>
+    <display_length compare="ge">768</display_length>
+  </requires>
+
+  <url type="homepage">https://pixieditor.net</url>
+  <url type="bugtracker">https://github.com/PixiEditor/PixiEditor/issues</url>
+  <url type="donation">https://pixieditor.net/download</url>
+  <url type="contact">https://pixieditor.net/help</url>
+  <url type="faq">https://pixieditor.net/docs/faq</url>
+  <url type="contribute">https://github.com/PixiEditor/PixiEditor</url>
+  <url type="vcs-browser">https://github.com/PixiEditor/PixiEditor</url>
+
+  <icon type="stock">net.pixieditor.PixiEditor</icon>
+
+  <branding>
+    <color type="primary" scheme_preference="light">#dedede</color>
+    <color type="primary" scheme_preference="dark">#1a1a1a</color>
+  </branding>
+
+  <content_rating type="oars-1.1" />
+
+  <releases>
+    <release version="2.0.1.16" date="2025-10-06">
+      <url type="details">https://forum.pixieditor.net/t/changelog-2-0-1-16/461</url>
+      <description>
+        <p>Posterize and Text Nodes, transforming improvements and new languagesW</p>
+      </description>      
+    </release>
+    <release version="2.0.1.14" date="2025-09-10">
+      <url type="details">https://forum.pixieditor.net/t/changelog-2-0-1-14/439/1</url>
+      <description>
+        <p>Fixed rectangle rendering issues</p>
+      </description>      
+    </release>
+  </releases>
+  
+  <description>
+    <p>
+	PixiEditor is a universal 2D editor designed for all kinds of creative work. Whether you want to make game sprites, paint illustrations, design logos, edit images, or create animations, PixiEditor gives you the tools to bring your ideas to life—all in a clean and familiar interface.
+    </p>
+    <p>
+	Powered by a Node Graph for advanced, non-destructive editing, PixiEditor comes with three unique toolsets that can be used together on the same canvas:
+    </p>
+    <p>
+	Pixel Art – pixel-perfect drawing tools for sprites and retro graphics
+	Painting – soft brushes, smooth lines, and anti-aliased shapes
+		    Vector – scalable paths and shapes for logos and clean designs
+    </p>
+  </description>
+  
+  <launchable type="desktop-id">net.pixieditor.PixiEditor.desktop</launchable>
+  <screenshots>
+    <screenshot type="default">
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/anim.png</image>
+	    <caption>Frame-by-frame animation with onion skinning</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/graph.png</image>
+	    <caption>Procedurally generated islands</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/palettes.png</image>
+	    <caption>Palette browser</caption>
+    </screenshot>
+    <screenshot>
+	    <image>https://raw.githubusercontent.com/pixieditor/pixieditor/refs/heads/flatpak/assets/flatpak/screenshots/vector.png</image>
+	    <caption>Vectors editing tools</caption>
+    </screenshot>
+  </screenshots>
+</component>

BIN
assets/flatpak/screenshots/anim.png


BIN
assets/flatpak/screenshots/graph.png


BIN
assets/flatpak/screenshots/palettes.png


BIN
assets/flatpak/screenshots/vector.png


+ 22 - 11
src/ChunkyImageLib/Chunk.cs

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
@@ -23,7 +24,7 @@ public class Chunk : IDisposable
     /// <summary>
     /// <summary>
     /// The surface of the chunk
     /// The surface of the chunk
     /// </summary>
     /// </summary>
-    public Surface Surface
+    public Texture Surface
     {
     {
         get
         get
         {
         {
@@ -50,7 +51,7 @@ public class Chunk : IDisposable
 
 
     public bool Disposed => returned;
     public bool Disposed => returned;
 
 
-    private Surface internalSurface;
+    private Texture internalSurface;
 
 
     private Chunk(ChunkResolution resolution, ColorSpace colorSpace)
     private Chunk(ChunkResolution resolution, ColorSpace colorSpace)
     {
     {
@@ -59,7 +60,7 @@ public class Chunk : IDisposable
         Resolution = resolution;
         Resolution = resolution;
         ColorSpace = colorSpace;
         ColorSpace = colorSpace;
         PixelSize = new(size, size);
         PixelSize = new(size, size);
-        internalSurface = new Surface(new ImageInfo(size, size, ColorType.RgbaF16, AlphaType.Premul, colorSpace));
+        internalSurface = new Texture(new ImageInfo(size, size, ColorType.RgbaF16, AlphaType.Premul, colorSpace) { GpuBacked = true });
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -67,7 +68,10 @@ public class Chunk : IDisposable
     /// </summary>
     /// </summary>
     public static Chunk Create(ColorSpace chunkCs, ChunkResolution resolution = ChunkResolution.Full)
     public static Chunk Create(ColorSpace chunkCs, ChunkResolution resolution = ChunkResolution.Full)
     {
     {
-        var chunk = ChunkPool.Instance.Get(resolution, chunkCs);
+        return new Chunk(resolution, chunkCs);
+
+        // Leaving this in case chunk pooling turns out to be better
+        /*var chunk = ChunkPool.Instance.Get(resolution, chunkCs);
         if (chunk == null || chunk.Disposed)
         if (chunk == null || chunk.Disposed)
         {
         {
             chunk = new Chunk(resolution, chunkCs);
             chunk = new Chunk(resolution, chunkCs);
@@ -75,7 +79,7 @@ public class Chunk : IDisposable
 
 
         chunk.returned = false;
         chunk.returned = false;
         Interlocked.Increment(ref chunkCounter);
         Interlocked.Increment(ref chunkCounter);
-        return chunk;
+        return chunk;*/
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -83,21 +87,23 @@ public class Chunk : IDisposable
     /// </summary>
     /// </summary>
     /// <param name="pos">The destination for the <paramref name="surface"/></param>
     /// <param name="pos">The destination for the <paramref name="surface"/></param>
     /// <param name="paint">The paint to use while drawing</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)
         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
         else
         {
         {
             using var snapshot = Surface.DrawingSurface.Snapshot();
             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);
         }
         }
     }
     }
 
 
     public unsafe RectI? FindPreciseBounds(RectI? passedSearchRegion = null)
     public unsafe RectI? FindPreciseBounds(RectI? passedSearchRegion = null)
     {
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         RectI? bounds = null;
         RectI? bounds = null;
         if (returned)
         if (returned)
             return bounds;
             return bounds;
@@ -109,7 +115,8 @@ public class Chunk : IDisposable
 
 
         RectI searchRegion = passedSearchRegion ?? new RectI(VecI.Zero, Surface.Size);
         RectI searchRegion = passedSearchRegion ?? new RectI(VecI.Zero, Surface.Size);
 
 
-        ulong* ptr = (ulong*)Surface.PixelBuffer;
+        using var pixmap = Surface.PeekPixels();
+        ulong* ptr = (ulong*)pixmap.GetPixels();
         for (int y = searchRegion.Top; y < searchRegion.Bottom; y++)
         for (int y = searchRegion.Top; y < searchRegion.Bottom; y++)
         {
         {
             for (int x = searchRegion.Left; x < searchRegion.Right; x++)
             for (int x = searchRegion.Left; x < searchRegion.Right; x++)
@@ -133,11 +140,15 @@ public class Chunk : IDisposable
     /// </summary>
     /// </summary>
     public void Dispose()
     public void Dispose()
     {
     {
-        if (returned)
+        returned = true;
+        internalSurface.Dispose();
+        // Leaving this in case chunk pooling turns out to be better
+        /*if (returned)
             return;
             return;
         Interlocked.Decrement(ref chunkCounter);
         Interlocked.Decrement(ref chunkCounter);
         Surface.DrawingSurface.Canvas.Clear();
         Surface.DrawingSurface.Canvas.Clear();
+        Surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
         ChunkPool.Instance.Push(this);
         ChunkPool.Instance.Push(this);
-        returned = true;
+        returned = true;*/
     }
     }
 }
 }

+ 123 - 25
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
@@ -6,6 +7,7 @@ using OneOf;
 using OneOf.Types;
 using OneOf.Types;
 using PixiEditor.Common;
 using PixiEditor.Common;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
@@ -193,7 +195,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
     /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
     /// </summary>
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full, bool fallbackToChunkAligned = false)
+    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
+        bool fallbackToChunkAligned = false)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
@@ -268,10 +271,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
                 return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
             }
             }
 
 
-            /*if (lastLatestBoundsCacheHash == GetCacheHash())
+            if (lastLatestBoundsCacheHash == GetCacheHash())
             {
             {
                 return cachedPreciseLatestBounds;
                 return cachedPreciseLatestBounds;
-            }*/
+            }
 
 
             var chunkSize = suggestedResolution.PixelSize();
             var chunkSize = suggestedResolution.PixelSize();
             var multiplier = suggestedResolution.Multiplier();
             var multiplier = suggestedResolution.Multiplier();
@@ -381,7 +384,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 var image = GetCommittedChunk(chunk, ChunkResolution.Full);
                 var image = GetCommittedChunk(chunk, ChunkResolution.Full);
                 if (image is null)
                 if (image is null)
                     continue;
                     continue;
-                output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
+                output.EnqueueDrawTexture(chunk * FullChunkSize, image.Surface);
             }
             }
 
 
             output.CommitChanges();
             output.CommitChanges();
@@ -454,6 +457,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
 
             // something is queued, blend mode is not Src so we have to do merging
             // something is queued, blend mode is not Src so we have to do merging
             {
             {
+                using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
                 Color committedColor = committedChunk is null
                 Color committedColor = committedChunk is null
@@ -467,8 +471,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
                 using Chunk tempChunk = Chunk.Create(ProcessingColorSpace, ChunkResolution.Eighth);
                 using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
                 using Paint committedPaint = new Paint() { Color = committedColor, BlendMode = BlendMode.Src };
                 using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
                 using Paint latestPaint = new Paint() { Color = latestColor, BlendMode = this.blendMode };
-                tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, committedPaint);
-                tempChunk.Surface.DrawingSurface.Canvas.DrawPixel(VecI.Zero, latestPaint);
+                tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecD(1)), committedPaint);
+                tempChunk.Surface.DrawingSurface.Canvas.DrawRect(new RectD(VecI.Zero, new VecI(1)), latestPaint);
                 return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
                 return tempChunk.Surface.GetSrgbPixel(VecI.Zero);
             }
             }
         }
         }
@@ -478,7 +482,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// True if the chunk existed and was drawn, otherwise false
     /// True if the chunk existed and was drawn, otherwise false
     /// </returns>
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <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)
         Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
     {
         lock (lockObject)
         lock (lockObject)
@@ -520,6 +524,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 return false;
                 return false;
             }
             }
 
 
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             // combine with committed and then draw
             // combine with committed and then draw
             using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
             using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
             tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
             tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
@@ -535,6 +540,66 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
         }
     }
     }
 
 
+    public bool DrawCachedMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, Canvas surface,
+        VecD pos,
+        Paint? paint = null, SamplingOptions? sampling = null)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            OneOf<None, EmptyChunk, Chunk> latestChunk;
+            {
+                var chunk = MaybeGetLatestChunk(chunkPos, resolution);
+                if (latestChunksData[resolution].TryGetValue(chunkPos, out var chunkData) && chunkData.IsDeleted)
+                {
+                    latestChunk = new EmptyChunk();
+                }
+                else
+                {
+                    latestChunk = chunk is null ? new None() : chunk;
+                }
+            }
+
+            var committedChunk = GetCommittedChunk(chunkPos, resolution);
+
+            // draw committed directly
+            if (latestChunk.IsT0 || latestChunk.IsT1 && committedChunk is not null && blendMode != BlendMode.Src)
+            {
+                if (committedChunk is null)
+                    return false;
+                committedChunk.DrawChunkOn(surface, pos, paint, sampling);
+                return true;
+            }
+
+            // no need to combine with committed, draw directly
+            if (blendMode == BlendMode.Src || committedChunk is null)
+            {
+                if (latestChunk.IsT2)
+                {
+                    latestChunk.AsT2.DrawChunkOn(surface, pos, paint, sampling);
+                    return true;
+                }
+
+                return false;
+            }
+
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
+            // combine with committed and then draw
+            using var tempChunk = Chunk.Create(ProcessingColorSpace, resolution);
+            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
+                ReplacingPaint);
+            blendModePaint.BlendMode = blendMode;
+            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
+                blendModePaint);
+            if (lockTransparency)
+                OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
+            tempChunk.DrawChunkOn(surface, pos, paint, sampling);
+
+            return true;
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public bool LatestOrCommittedChunkExists(VecI chunkPos)
     public bool LatestOrCommittedChunkExists(VecI chunkPos)
     {
     {
@@ -571,7 +636,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     }
     }
 
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <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)
         Paint? paint = null, SamplingOptions? samplingOptions = null)
     {
     {
         lock (lockObject)
         lock (lockObject)
@@ -856,6 +921,20 @@ 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);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
     public void EnqueueDrawBresenhamLine(VecI from, VecI to, Paintable paintable, BlendMode blendMode)
     {
     {
@@ -911,17 +990,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>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     public void EnqueueDrawCommitedChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {
     {
@@ -1069,6 +1137,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
             ThrowIfDisposed();
             ThrowIfDisposed();
             var affectedArea = FindAffectedArea();
             var affectedArea = FindAffectedArea();
 
 
@@ -1159,6 +1228,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                         continue;
                         continue;
                     }
                     }
 
 
+                    using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
                     //blend
                     //blend
                     blendModePaint.BlendMode = blendMode;
                     blendModePaint.BlendMode = blendMode;
                     if (lockTransparency)
                     if (lockTransparency)
@@ -1238,6 +1309,28 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         }
         }
     }
     }
 
 
+    public Dictionary<VecI, Surface> CloneAllCommitedNonEmptyChunks()
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+            var dict = new Dictionary<VecI, Surface>();
+            foreach (var (pos, chunk) in committedChunks[ChunkResolution.Full])
+            {
+                if (chunk.FindPreciseBounds().HasValue)
+                {
+                    var surf = new Surface(chunk.Surface.ImageInfo);
+                    surf.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0);
+                    dict[pos] = surf;
+                    surf.DrawingSurface.Flush();
+                }
+            }
+
+            return dict;
+        }
+    }
+
     /// <returns>
     /// <returns>
     /// Chunks affected by operations that haven't been committed yet
     /// Chunks affected by operations that haven't been committed yet
     /// </returns>
     /// </returns>
@@ -1345,7 +1438,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         {
         {
             if (mask.CommittedChunkExists(chunkPos))
             if (mask.CommittedChunkExists(chunkPos))
             {
             {
-                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero,
+                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface.Canvas, VecI.Zero,
                     ClippingPaint);
                     ClippingPaint);
             }
             }
             else
             else
@@ -1373,6 +1466,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         if (operation is ClearOperation)
         if (operation is ClearOperation)
             return true;
             return true;
 
 
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
+
         if (operation is IDrawOperation chunkOperation)
         if (operation is IDrawOperation chunkOperation)
         {
         {
             if (combinedRasterClips.IsT1) // Nothing is visible
             if (combinedRasterClips.IsT1) // Nothing is visible
@@ -1392,14 +1487,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             var clip = combinedRasterClips.AsT2;
             var clip = combinedRasterClips.AsT2;
 
 
             using var tempChunk = Chunk.Create(ProcessingColorSpace, targetChunk.Resolution);
             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);
             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;
             return false;
         }
         }
 
 
@@ -1498,6 +1593,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// </summary>
     /// </summary>
     private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
     private Chunk GetOrCreateCommittedChunk(VecI chunkPos, ChunkResolution resolution)
     {
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         // committed chunk of the same resolution exists
         // committed chunk of the same resolution exists
         Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
         Chunk? targetChunk = MaybeGetCommittedChunk(chunkPos, resolution);
         if (targetChunk is not null)
         if (targetChunk is not null)
@@ -1519,7 +1615,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             newChunk.Surface.DrawingSurface.Canvas.Save();
             newChunk.Surface.DrawingSurface.Canvas.Save();
             newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
             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);
                 SmoothReplacingPaint);
             newChunk.Surface.DrawingSurface.Canvas.Restore();
             newChunk.Surface.DrawingSurface.Canvas.Restore();
             committedChunks[resolution][chunkPos] = newChunk;
             committedChunks[resolution][chunkPos] = newChunk;
@@ -1540,6 +1637,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// </summary>
     /// </summary>
     private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
     private Chunk GetOrCreateLatestChunk(VecI chunkPos, ChunkResolution resolution)
     {
     {
+        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         // latest chunk exists
         // latest chunk exists
         Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
         Chunk? targetChunk = MaybeGetLatestChunk(chunkPos, resolution);
         if (targetChunk is not null)
         if (targetChunk is not null)

+ 94 - 15
src/ChunkyImageLib/ChunkyImageEx.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using System.Diagnostics;
+using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
@@ -6,6 +7,7 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib;
 namespace ChunkyImageLib;
+
 public static class IReadOnlyChunkyImageEx
 public static class IReadOnlyChunkyImageEx
 {
 {
     /// <summary>
     /// <summary>
@@ -19,12 +21,30 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawMostUpToDateRegionOn
     public static void DrawMostUpToDateRegionOn
-    (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface,
-        VecD pos, Paint? paint = null, SamplingOptions? sampling = null)
+    (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);
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn, paint, sampling, drawPaintOnEmpty);
     }
     }
-    
+
+    /// <summary>
+    /// Extracts a region from the <see cref="ChunkyImage"/> and draws it onto the passed <see cref="DrawingSurface"/>.
+    /// The region is taken from the most up to date version of the <see cref="ChunkyImage"/>
+    /// </summary>
+    /// <param name="image"><see cref="ChunkyImage"/> to extract the region from</param>
+    /// <param name="fullResRegion">The region to extract</param>
+    /// <param name="resolution">Chunk resolution</param>
+    /// <param name="surface">Surface to draw onto</param>
+    /// <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, Canvas surface,
+        AffectedArea affectedArea, VecD pos, Paint? paint = null, SamplingOptions? sampling = null, bool drawPaintOnEmpty = false)
+    {
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawMostUpToDateChunkOn,
+            image.DrawCachedMostUpToDateChunkOn, affectedArea, paint, sampling, drawPaintOnEmpty);
+    }
+
     /// <summary>
     /// <summary>
     /// Extracts a region from the <see cref="ChunkyImage"/> and draws it onto the passed <see cref="DrawingSurface"/>.
     /// Extracts a region from the <see cref="ChunkyImage"/> and draws it onto the passed <see cref="DrawingSurface"/>.
     /// The region is taken from the committed version of the <see cref="ChunkyImage"/>
     /// The region is taken from the committed version of the <see cref="ChunkyImage"/>
@@ -36,21 +56,59 @@ public static class IReadOnlyChunkyImageEx
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="pos">Starting position on the surface</param>
     /// <param name="paint">Paint to use for drawing</param>
     /// <param name="paint">Paint to use for drawing</param>
     public static void DrawCommittedRegionOn
     public static void DrawCommittedRegionOn
-        (this IReadOnlyChunkyImage image, RectI fullResRegion, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null, SamplingOptions? samplingOptions = null)
+    (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);
+        DrawRegionOn(fullResRegion, resolution, surface, pos, image.DrawCommittedChunkOn, paint, samplingOptions, drawPaintOnEmpty);
     }
     }
-    
+
+    private static void DrawRegionOn(
+        RectI fullResRegion,
+        ChunkResolution resolution,
+        Canvas surface,
+        VecD pos,
+        Func<VecI, ChunkResolution, Canvas, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
+        Paint? paint = null, SamplingOptions? samplingOptions = null, bool drawPaintOnEmpty = false)
+    {
+        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);
+        VecI offsetFullRes = (chunkTopLeft * ChunkyImage.FullChunkSize) - fullResRegion.Pos;
+        VecI offsetTargetRes = (VecI)(offsetFullRes * resolution.Multiplier());
+
+        for (int j = chunkTopLeft.Y; j <= chunkBotRight.Y; j++)
+        {
+            for (int i = chunkTopLeft.X; i <= chunkBotRight.X; i++)
+            {
+                var chunkPos = new VecI(i, j);
+                if (!drawingFunc(chunkPos, resolution, surface,
+                        offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                        samplingOptions) && paint != null && drawPaintOnEmpty)
+                {
+                    surface.DrawRect(new RectD(
+                        offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                        new VecD(resolution.PixelSize())), paint);
+                }
+            }
+        }
+
+        surface.RestoreToCount(count);
+    }
+
     private static void DrawRegionOn(
     private static void DrawRegionOn(
         RectI fullResRegion,
         RectI fullResRegion,
         ChunkResolution resolution,
         ChunkResolution resolution,
-        DrawingSurface surface,
+        Canvas surface,
         VecD pos,
         VecD pos,
-        Func<VecI, ChunkResolution, DrawingSurface, VecD, Paint?, SamplingOptions?, bool> drawingFunc,
-        Paint? paint = null, SamplingOptions? samplingOptions = null)
+        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 chunkTopLeft = OperationHelper.GetChunkPos(fullResRegion.TopLeft, ChunkyImage.FullChunkSize);
         VecI chunkBotRight = OperationHelper.GetChunkPos(fullResRegion.BottomRight, ChunkyImage.FullChunkSize);
         VecI chunkBotRight = OperationHelper.GetChunkPos(fullResRegion.BottomRight, ChunkyImage.FullChunkSize);
@@ -62,10 +120,31 @@ public static class IReadOnlyChunkyImageEx
             for (int i = chunkTopLeft.X; i <= chunkBotRight.X; i++)
             for (int i = chunkTopLeft.X; i <= chunkBotRight.X; i++)
             {
             {
                 var chunkPos = new VecI(i, j);
                 var chunkPos = new VecI(i, j);
-                drawingFunc(chunkPos, resolution, surface, offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint, samplingOptions);
+                if (area.Chunks != null && area.Chunks.Contains(chunkPos))
+                {
+                    if (!drawingFunc(chunkPos, resolution, surface,
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                            samplingOptions) && paint != null && drawPaintOnEmpty)
+                    {
+                        surface.DrawRect(new RectD(
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                            new VecD(resolution.PixelSize())), paint);
+                    }
+                }
+                else
+                {
+                    if (!quickDrawingFunc(chunkPos, resolution, surface,
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos, paint,
+                            samplingOptions) && paint != null && drawPaintOnEmpty)
+                    {
+                        surface.DrawRect(new RectD(
+                            offsetTargetRes + (chunkPos - chunkTopLeft) * resolution.PixelSize() + pos,
+                            new VecD(resolution.PixelSize())), paint);
+                    }
+                }
             }
             }
         }
         }
 
 
-        surface.Canvas.RestoreToCount(count);
+        surface.RestoreToCount(count);
     }
     }
 }
 }

+ 2 - 2
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -17,7 +17,7 @@ public class CommittedChunkStorage : IDisposable
         foreach (var chunkPos in committedChunksToSave)
         foreach (var chunkPos in committedChunksToSave)
         {
         {
             Chunk copy = Chunk.Create(image.ProcessingColorSpace);
             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();
                 copy.Dispose();
                 savedChunks.Add((chunkPos, null));
                 savedChunks.Add((chunkPos, null));
@@ -36,7 +36,7 @@ public class CommittedChunkStorage : IDisposable
             if (chunk is null)
             if (chunk is null)
                 image.EnqueueClearRegion(new(pos * ChunkPool.FullChunkSize, new(ChunkPool.FullChunkSize, ChunkPool.FullChunkSize)));
                 image.EnqueueClearRegion(new(pos * ChunkPool.FullChunkSize, new(ChunkPool.FullChunkSize, ChunkPool.FullChunkSize)));
             else
             else
-                image.EnqueueDrawImage(pos * ChunkPool.FullChunkSize, chunk.Surface, ReplacingPaint);
+                image.EnqueueDrawTexture(pos * ChunkPool.FullChunkSize, chunk.Surface, ReplacingPaint);
         }
         }
     }
     }
 
 

+ 3 - 2
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -10,8 +10,9 @@ namespace ChunkyImageLib;
 
 
 public interface IReadOnlyChunkyImage
 public interface IReadOnlyChunkyImage
 {
 {
-    bool DrawMostUpToDateChunkOn(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? FindChunkAlignedMostUpToDateBounds();
     RectI? FindChunkAlignedCommittedBounds();
     RectI? FindChunkAlignedCommittedBounds();
     RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full, bool fallbackToChunkAligned = false);
     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)
     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()
     public void Dispose()

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

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

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

@@ -63,12 +63,12 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
         VecI bottomLeft = OperationHelper.GetChunkPos(
         VecI bottomLeft = OperationHelper.GetChunkPos(
             new VecI(chunkCenterOnImage.X - halfChunk.X, chunkCenterOnImage.Y + halfChunk.Y), ChunkyImage.FullChunkSize);
             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(
         drawMethod(
             topLeft,
             topLeft,
             targetChunk.Resolution,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((topLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()), null, null);
             (VecI)((topLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()), null, null);
 
 
         VecI gridShift = targetPos % ChunkyImage.FullChunkSize;
         VecI gridShift = targetPos % ChunkyImage.FullChunkSize;
@@ -77,7 +77,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             drawMethod(
             topRight,
             topRight,
             targetChunk.Resolution,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((topRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             (VecI)((topRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
             null, null);
         }
         }
@@ -86,7 +86,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             drawMethod(
             bottomLeft,
             bottomLeft,
             targetChunk.Resolution,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((bottomLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             (VecI)((bottomLeft * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
             null, null);
         }
         }
@@ -95,7 +95,7 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
             drawMethod(
             drawMethod(
             bottomRight,
             bottomRight,
             targetChunk.Resolution,
             targetChunk.Resolution,
-            targetChunk.Surface.DrawingSurface,
+            targetChunk.Surface.DrawingSurface.Canvas,
             (VecI)((bottomRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             (VecI)((bottomRight * ChunkyImage.FullChunkSize - chunkCenterOnImage).Add(ChunkyImage.FullChunkSize / 2) * targetChunk.Resolution.Multiplier()),
             null, null);
             null, null);
         }
         }

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

@@ -84,7 +84,13 @@ internal class ImageOperation : IMirroredDrawOperation
 
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     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();
         float scaleMult = (float)targetChunk.Resolution.Multiplier();
         VecD trans = -chunkPos * ChunkPool.FullChunkSize;
         VecD trans = -chunkPos * ChunkPool.FullChunkSize;
 
 
@@ -104,12 +110,12 @@ internal class ImageOperation : IMirroredDrawOperation
             ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
             ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
             RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
             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
         else
         {
         {
             // Slower, but works with perspective transformation
             // 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();
         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;
+    }
+}

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

@@ -1,5 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -14,6 +15,8 @@ internal class PathOperation : IMirroredDrawOperation
     private readonly Paint paint;
     private readonly Paint paint;
     private readonly RectI bounds;
     private readonly RectI bounds;
 
 
+    private bool antiAliasing;
+
     public bool IgnoreEmptyChunks => false;
     public bool IgnoreEmptyChunks => false;
 
 
     public PathOperation(VectorPath path, Color color, float strokeWidth, StrokeCap cap, BlendMode blendMode, RectI? customBounds = null)
     public PathOperation(VectorPath path, Color color, float strokeWidth, StrokeCap cap, BlendMode blendMode, RectI? customBounds = null)
@@ -25,9 +28,20 @@ internal class PathOperation : IMirroredDrawOperation
         bounds = floatBounds.Inflate((int)Math.Ceiling(strokeWidth) + 1);
         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 void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     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;
         var surf = targetChunk.Surface.DrawingSurface;
         surf.Canvas.Save();
         surf.Canvas.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
@@ -52,6 +66,12 @@ internal class PathOperation : IMirroredDrawOperation
             newBounds = (RectI)newBounds.ReflectX((double)verAxisX).Round();
             newBounds = (RectI)newBounds.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
             newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
             newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
+        if (paint.Paintable != null)
+        {
+            return new PathOperation(copy, paint.Paintable, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode,
+                paint.Style, antiAliasing, newBounds);
+        }
+
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, 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;
 namespace ChunkyImageLib.Operations;
 
 
-public delegate Color PixelProcessor(Color commited, Color upToDate);
 internal class PixelOperation : IMirroredDrawOperation
 internal class PixelOperation : IMirroredDrawOperation
 {
 {
     public bool IgnoreEmptyChunks => false;
     public bool IgnoreEmptyChunks => false;
@@ -15,9 +14,6 @@ internal class PixelOperation : IMirroredDrawOperation
     private readonly Color color;
     private readonly Color color;
     private readonly BlendMode blendMode;
     private readonly BlendMode blendMode;
     private readonly Paint paint;
     private readonly Paint paint;
-    private readonly Func<VecI, Color>? getCommitedPixelFunc = null;
-
-    private readonly PixelProcessor? colorProcessor = null;
 
 
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     {
     {
@@ -27,15 +23,6 @@ internal class PixelOperation : IMirroredDrawOperation
         paint = new Paint() { BlendMode = blendMode };
         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)
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
     {
     {
         // a hacky way to make the lines look slightly better on non full res chunks
         // 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.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
         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();
         surf.Canvas.Restore();
     }
     }
 
 
     private Color GetColor(Chunk chunk, VecI chunkPos)
     private Color GetColor(Chunk chunk, VecI chunkPos)
     {
     {
         Color pixelColor = color;
         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()));
         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();
             pixelRect = (RectI)pixelRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
         if (horAxisY is not null)
             pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY);
             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);
         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.Save();
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Scale((float)targetChunk.Resolution.Multiplier());
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
         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();
         surf.Canvas.Restore();
     }
     }
 
 

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

@@ -193,7 +193,7 @@ internal class RectangleOperation : IMirroredDrawOperation
     public AffectedArea FindAffectedArea(VecI imageSize)
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
     {
         if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 ||
         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();
             return new();
 
 
         RectI affRect = (RectI)new ShapeCorners(Data.Center, Data.Size).AsRotated(Data.Angle, Data.Center).AABBBounds
         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 ChunkyImageLib.DataHolders;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
@@ -89,7 +90,8 @@ internal class TextureOperation : IMirroredDrawOperation
 
 
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
         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();
         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>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
         <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
         <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.6</AvaloniaVersion>
+		    <AvaloniaVersion>11.3.9-cibuild0004033-alpha</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
   
   
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit f3317c6a85fc3f463490ff9377c6a15f4ad766c6
+Subproject commit 322feadde26aae236f790702e712141f8f1d70a5

+ 1 - 1
src/PixiDocks

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

+ 8 - 14
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -33,7 +33,10 @@ public class FFMpegRenderer : IAnimationRenderer
             MakeExecutableIfNeeded(binaryPath);
             MakeExecutableIfNeeded(binaryPath);
         }
         }
 
 
-        string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+        string tempPath = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
+        Directory.CreateDirectory(tempPath);
+
+        string paletteTempPath = Path.Combine(tempPath, "palette.png");
 
 
         try
         try
         {
         {
@@ -46,12 +49,6 @@ public class FFMpegRenderer : IAnimationRenderer
 
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
 
-
-            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
-            {
-                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
-            }
-
             if (RequiresPaletteGeneration())
             if (RequiresPaletteGeneration())
             {
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);
                 GeneratePalette(streamPipeSource, paletteTempPath);
@@ -98,7 +95,10 @@ public class FFMpegRenderer : IAnimationRenderer
             MakeExecutableIfNeeded(binaryPath);
             MakeExecutableIfNeeded(binaryPath);
         }
         }
 
 
-        string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+        string tempPath = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
+        Directory.CreateDirectory(tempPath);
+
+        string paletteTempPath = Path.Combine(tempPath, "palette.png");
 
 
         try
         try
         {
         {
@@ -111,12 +111,6 @@ public class FFMpegRenderer : IAnimationRenderer
 
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
 
-
-            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
-            {
-                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
-            }
-
             if (RequiresPaletteGeneration())
             if (RequiresPaletteGeneration())
             {
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);
                 GeneratePalette(streamPipeSource, paletteTempPath);

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

@@ -14,7 +14,7 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <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">
     <PackageReference Include="Mono.Cecil" Version="0.11.6">
       <PrivateAssets>All</PrivateAssets>
       <PrivateAssets>All</PrivateAssets>
       <IncludeAssets>All</IncludeAssets>
       <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(
         return new CreateFolder_ChangeInfo(
             folder.GetNodeTypeUniqueName(),
             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; }
     public bool LockTransparency { get; }
 
 
-    internal static CreateLayer_ChangeInfo FromLayer(LayerNode layer)
+    public static CreateLayer_ChangeInfo FromLayer(LayerNode layer)
     {
     {
         return new CreateLayer_ChangeInfo(
         return new CreateLayer_ChangeInfo(
             layer.GetNodeTypeUniqueName(),
             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);
         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,
     private bool TryFindKeyFrameCallback<T>(Guid id, out T? foundKeyFrame,
         Action<KeyFrame, GroupKeyFrame?> onFound = null) where T : IReadOnlyKeyFrame
         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;
+    }
+}

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

@@ -0,0 +1,521 @@
+using System.Diagnostics;
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
+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 int lastAppliedPointIndex = -1;
+    private VecI lastCachedTexturePaintableSize = VecI.Zero;
+    private TexturePaintable? lastCachedTexturePaintable = null;
+
+    private bool drawnOnce = false;
+
+    public void ResetState()
+    {
+        lastAppliedPointIndex = -1;
+        drawnOnce = false;
+    }
+
+    public void ExecuteBrush(ChunkyImage target, BrushData brushData, List<VecD> points, KeyFrameTime frameTime,
+        ColorSpace cs, SamplingOptions samplingOptions, PointerInfo pointerInfo, KeyboardInfo keyboardInfo,
+        EditorData editorData)
+    {
+        if (brushData.BrushGraph == null)
+        {
+            return;
+        }
+
+        if (brushData.BrushGraph.LookupNode(brushData.TargetBrushNodeId) is not BrushOutputNode brushNode)
+        {
+            return;
+        }
+
+        float strokeWidth = brushData.StrokeWidth;
+        float spacing = brushNode.Spacing.Value / 100f;
+
+        float spacingPixels = (strokeWidth * pointerInfo.Pressure) * spacing;
+
+        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+        {
+            var point = points[i];
+            if (VecD.Distance(lastPos, point) < spacingPixels)
+                continue;
+
+            ExecuteVectorShapeBrush(target, brushNode, brushData, point, frameTime, cs, samplingOptions, pointerInfo,
+                keyboardInfo,
+                editorData);
+
+            lastPos = point;
+        }
+
+        lastAppliedPointIndex = points.Count - 1;
+    }
+
+    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;
+        }
+
+        float strokeWidth = brushData.StrokeWidth;
+        float spacing = brushNode.Spacing.Value / 100f;
+
+        for (int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
+        {
+            var point = points[i];
+
+            float spacingPixels = (strokeWidth * point.PointerInfo.Pressure) * spacing;
+            if (VecD.Distance(lastPos, point.Position) < spacingPixels)
+                continue;
+
+            ExecuteVectorShapeBrush(target, brushNode, brushData, point.Position, frameTime, cs, samplingOptions,
+                point.PointerInfo,
+                point.KeyboardInfo,
+                point.EditorData);
+
+            lastPos = point.Position;
+        }
+
+        lastAppliedPointIndex = points.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;
+
+        if (PaintBrush(target, autoPosition, vectorShape, rect, fitToStrokeSize, pressure, content, contentTexture,
+                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, 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 })
+        {
+            target.EnqueueDrawPath(path, paintable, vectorShape.StrokeWidth,
+                strokeCap, blendMode, strokeStyle, antiAliasing, null);
+        }
+
+        if (fill is { AnythingVisible: true } && stroke is { AnythingVisible: true })
+        {
+            strokeStyle = PaintStyle.Stroke;
+            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);
+                }
+
+                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.Numerics;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables;
 namespace PixiEditor.ChangeableDocument.Changeables;
 
 
 internal class Document : IChangeable, IReadOnlyDocument
 internal class Document : IChangeable, IReadOnlyDocument
 {
 {
-    public Guid DocumentId { get; } = Guid.NewGuid();
+    public Guid DocumentId { get; private set; } = Guid.NewGuid();
     IReadOnlyNodeGraph IReadOnlyDocument.NodeGraph => NodeGraph;
     IReadOnlyNodeGraph IReadOnlyDocument.NodeGraph => NodeGraph;
     IReadOnlySelection IReadOnlyDocument.Selection => Selection;
     IReadOnlySelection IReadOnlyDocument.Selection => Selection;
     IReadOnlyAnimationData IReadOnlyDocument.AnimationData => AnimationData;
     IReadOnlyAnimationData IReadOnlyDocument.AnimationData => AnimationData;
@@ -32,6 +33,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
 
     IReadOnlyReferenceLayer? IReadOnlyDocument.ReferenceLayer => ReferenceLayer;
     IReadOnlyReferenceLayer? IReadOnlyDocument.ReferenceLayer => ReferenceLayer;
     public DocumentRenderer Renderer { get; }
     public DocumentRenderer Renderer { get; }
+    public IReadOnlyBlackboard Blackboard => NodeGraph.Blackboard;
     public ColorSpace ProcessingColorSpace { get; internal set; } = ColorSpace.CreateSrgbLinear();
     public ColorSpace ProcessingColorSpace { get; internal set; } = ColorSpace.CreateSrgbLinear();
 
 
     /// <summary>
     /// <summary>
@@ -39,10 +41,10 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// </summary>
     /// </summary>
     public static VecI DefaultSize { get; } = new VecI(64, 64);
     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 ReferenceLayer? ReferenceLayer { get; set; }
-    internal AnimationData AnimationData { get; }
+    internal AnimationData AnimationData { get; private set; }
     public VecI Size { get; set; } = DefaultSize;
     public VecI Size { get; set; } = DefaultSize;
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
@@ -104,7 +106,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             chunkyImage.DrawCommittedRegionOn(
             chunkyImage.DrawCommittedRegionOn(
                 new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 ChunkResolution.Full,
                 ChunkResolution.Full,
-                chunkSurface.DrawingSurface,
+                chunkSurface.DrawingSurface.Canvas,
                 VecI.Zero);
                 VecI.Zero);
 
 
             image = chunkSurface;
             image = chunkSurface;
@@ -173,6 +175,42 @@ internal class Document : IChangeable, IReadOnlyDocument
         return new DocumentNodePipe<T>(this, layerId);
         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)
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
     {
         graph.TryTraverse((node) =>
         graph.TryTraverse((node) =>
@@ -444,7 +482,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
 
     private void ExtractLayers(FolderNode folder, List<Guid> list)
     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 =>
         folder.Content.Connection.Node.TraverseBackwards(node =>
         {
         {
             if (node is LayerNode layer && !list.Contains(layer.Id))
             if (node is LayerNode layer && !list.Contains(layer.Id))
@@ -455,4 +493,9 @@ internal class Document : IChangeable, IReadOnlyDocument
             return true;
             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)
         if (!HasContext && first is Int1 firstInt && second is Int1 secondInt)
         {
         {
             Int2 constantInt = new Int2("");
             Int2 constantInt = new Int2("");
-            constantInt.ConstantValue = new VecI(firstInt.ConstantValue, secondInt.ConstantValue);
+            constantInt.ConstantValue = new VecI(GetIntConstant(firstInt), GetIntConstant(secondInt));
             return constantInt;
             return constantInt;
         }
         }
 
 
         return Builder.ConstructInt2(first, second);
         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)
     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 &&
         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))
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
             {
+                float value = 0;
+                if (getFrom?.Value != null)
+                {
+                    value = (float)getFrom.Value(this).ConstantValue;
+                }
+
                 string uniformName = $"float_{Builder.GetUniqueNameNumber()}";
                 string uniformName = $"float_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(uniformName, (float)getFrom.Value(this).ConstantValue);
+                Builder.AddUniform(uniformName, value);
                 return new Float1(uniformName);
                 return new Float1(uniformName);
             }
             }
 
 
@@ -225,8 +246,14 @@ public class FuncContext
         {
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             {
             {
+                int value = 0;
+                if (getFrom?.Value != null)
+                {
+                    value = getFrom.Value(this)?.ConstantValue ?? 0;
+                }
+
                 string uniformName = $"int_{Builder.GetUniqueNameNumber()}";
                 string uniformName = $"int_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(uniformName, (int)getFrom.Value(this).ConstantValue);
+                Builder.AddUniform(uniformName, value);
                 return new Int1(uniformName);
                 return new Int1(uniformName);
             }
             }
 
 
@@ -256,9 +283,9 @@ public class FuncContext
         {
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             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()}";
                 color.VariableName = $"color_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(color.VariableName, color.ConstantValue);
+                Builder.AddUniform(color.VariableName, color?.ConstantValue ?? Colors.Transparent);
                 return color;
                 return color;
             }
             }
 
 
@@ -283,9 +310,9 @@ public class FuncContext
         {
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             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()}";
                 value.VariableName = $"float2_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? VecD.Zero);
                 return value;
                 return value;
             }
             }
 
 
@@ -310,9 +337,9 @@ public class FuncContext
         {
         {
             if (getFrom?.Connection == null || !IsFuncType(getFrom))
             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()}";
                 value.VariableName = $"int2_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? VecI.Zero);
                 return value;
                 return value;
             }
             }
 
 
@@ -337,9 +364,9 @@ public class FuncContext
         {
         {
             if (getFrom.Connection == null || !IsFuncType(getFrom))
             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()}";
                 value.VariableName = $"float3x3_{Builder.GetUniqueNameNumber()}";
-                Builder.AddUniform(value.VariableName, value.ConstantValue);
+                Builder.AddUniform(value.VariableName, value?.ConstantValue ?? Matrix3X3.Identity);
                 return value;
                 return value;
             }
             }
 
 
@@ -382,7 +409,7 @@ public class FuncContext
         if (!HasContext && matrixExpression is Float3x3 float3x3)
         if (!HasContext && matrixExpression is Float3x3 float3x3)
         {
         {
             Float3x3 constantMatrix = new Float3x3("");
             Float3x3 constantMatrix = new Float3x3("");
-            constantMatrix.ConstantValue = float3x3.ConstantValue;
+            constantMatrix.ConstantValue = float3x3?.ConstantValue ?? Matrix3X3.Identity;
             return constantMatrix;
             return constantMatrix;
         }
         }
 
 

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

@@ -1,9 +1,10 @@
 using System.Diagnostics.Contracts;
 using System.Diagnostics.Contracts;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using PixiEditor.Common;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 
-public sealed class Filter : IDisposable
+public sealed class Filter : IDisposable, ICacheable
 {
 {
     public Filter(ColorFilter? colorFilter, ImageFilter? imageFilter)
     public Filter(ColorFilter? colorFilter, ImageFilter? imageFilter)
     {
     {
@@ -45,4 +46,12 @@ public sealed class Filter : IDisposable
         ColorFilter?.Dispose();
         ColorFilter?.Dispose();
         ImageFilter?.Dispose();
         ImageFilter?.Dispose();
     }
     }
+
+    public int GetCacheHash()
+    {
+        HashCode hash = new();
+        hash.Add(ColorFilter?.GetHashCode() ?? 0);
+        hash.Add(ImageFilter?.GetHashCode() ?? 0);
+        return hash.ToHashCode();
+    }
 }
 }

+ 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;
                 return _internalValue;
             }
             }
 
 
-            var connectionValue = Connection.Value;
+            var connectionValue = Connection?.Value;
 
 
             if (connectionValue is null)
             if (connectionValue is null)
             {
             {
@@ -265,6 +265,7 @@ public class InputProperty<T> : InputProperty, IInputProperty<T>
             if (value is ShaderExpressionVariable shaderExpression)
             if (value is ShaderExpressionVariable shaderExpression)
             {
             {
                 value = shaderExpression.GetConstant();
                 value = shaderExpression.GetConstant();
+                if (value is null) return default(T);
             }
             }
 
 
             if (!ConversionTable.TryConvert(value, ValueType, out object result))
             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 interface IClipSource
 {
 {
-    public void DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto);
+    public void DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto);
 }
 }

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

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

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

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

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

@@ -44,6 +44,6 @@ public interface IReadOnlyNode : ICacheable
 
 
     public IInputProperty? GetInputProperty(string internalName);
     public IInputProperty? GetInputProperty(string internalName);
     public IOutputProperty? GetOutputProperty(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();
     public string GetNodeTypeUniqueName();
 }
 }

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

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

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

@@ -8,7 +8,7 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 
-public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject, IChunkRenderable
+public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject
 {
 {
     public InputProperty<float> Opacity { get; }
     public InputProperty<float> Opacity { get; }
     public InputProperty<bool> IsVisible { get; }
     public InputProperty<bool> IsVisible { get; }
@@ -20,5 +20,5 @@ public interface IReadOnlyStructureNode : IReadOnlyNode, ISceneObject, IChunkRen
     public RectD? GetTightBounds(KeyFrameTime frameTime);
     public RectD? GetTightBounds(KeyFrameTime frameTime);
     public ChunkyImage? EmbeddedMask { get; }
     public ChunkyImage? EmbeddedMask { get; }
     public ShapeCorners GetTransformationCorners(KeyFrameTime frameTime);
     public ShapeCorners GetTransformationCorners(KeyFrameTime frameTime);
-    public void RenderForOutput(RenderContext context, DrawingSurface renderTarget, RenderOutputProperty output);
+    public void RenderForOutput(RenderContext context, Canvas renderTarget, RenderOutputProperty output);
 }
 }

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

@@ -12,11 +12,28 @@ public class SceneObjectRenderContext : RenderContext
     public bool RenderSurfaceIsScene { get; }
     public bool RenderSurfaceIsScene { get; }
     public RenderOutputProperty TargetPropertyOutput { 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;
         TargetPropertyOutput = targetPropertyOutput;
         LocalBounds = localBounds;
         LocalBounds = localBounds;
         RenderSurfaceIsScene = renderSurfaceIsScene;
         RenderSurfaceIsScene = renderSurfaceIsScene;
     }
     }
+
+    public override RenderContext Clone()
+    {
+        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,
+        };
+    }
 }
 }

+ 166 - 26
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -1,25 +1,39 @@
 using System.Collections.Immutable;
 using System.Collections.Immutable;
+using System.Diagnostics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 
-public class NodeGraph : IReadOnlyNodeGraph, IDisposable
+public class NodeGraph : IReadOnlyNodeGraph
 {
 {
-    private ImmutableList<IReadOnlyNode>? cachedExecutionList;
-    
+    private Dictionary<IReadOnlyNode, ImmutableList<IReadOnlyNode>?> cachedExecutionList;
+
     private readonly List<Node> _nodes = new();
     private readonly List<Node> _nodes = new();
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public IReadOnlyCollection<Node> Nodes => _nodes;
     public IReadOnlyDictionary<Guid, Node> NodeLookup => nodeLookup;
     public IReadOnlyDictionary<Guid, Node> NodeLookup => nodeLookup;
+
     public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
     public Node? OutputNode => CustomOutputNode ?? Nodes.OfType<OutputNode>().FirstOrDefault();
     public Node? CustomOutputNode { get; set; }
     public Node? CustomOutputNode { get; set; }
 
 
+    public Blackboard Blackboard { get; } = new();
+
     private Dictionary<Guid, Node> nodeLookup = new();
     private Dictionary<Guid, Node> nodeLookup = new();
 
 
+    public event Action<NodeOutputsChanged_ChangeInfo>? NodeOutputsChanged;
+
     IReadOnlyCollection<IReadOnlyNode> IReadOnlyNodeGraph.AllNodes => Nodes;
     IReadOnlyCollection<IReadOnlyNode> IReadOnlyNodeGraph.AllNodes => Nodes;
     IReadOnlyNode IReadOnlyNodeGraph.OutputNode => OutputNode;
     IReadOnlyNode IReadOnlyNodeGraph.OutputNode => OutputNode;
+    IReadOnlyBlackboard IReadOnlyNodeGraph.Blackboard => Blackboard;
 
 
+    bool isExecuting = false;
+
+    public IReadOnlyNode LookupNode(Guid guid)
+    {
+        return nodeLookup[guid];
+    }
 
 
     public void AddNode(Node node)
     public void AddNode(Node node)
     {
     {
@@ -27,9 +41,10 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
         {
         {
             return;
             return;
         }
         }
-        
+
         node.ConnectionsChanged += ResetCache;
         node.ConnectionsChanged += ResetCache;
         _nodes.Add(node);
         _nodes.Add(node);
+        node.OutputsChanged += () => NodeOutputsChanged?.Invoke(NodeOutputsChanged_ChangeInfo.FromNode(node));
         nodeLookup[node.Id] = node;
         nodeLookup[node.Id] = node;
         ResetCache();
         ResetCache();
     }
     }
@@ -61,10 +76,81 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     {
     {
         return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
         return new Queue<IReadOnlyNode>(CalculateExecutionQueueInternal(outputNode));
     }
     }
-    
+
+    public IReadOnlyNodeGraph Clone()
+    {
+        var newGraph = new NodeGraph();
+        var nodeMapping = new Dictionary<Node, Node>();
+
+        // Clone nodes
+        foreach (var node in Nodes)
+        {
+            var clonedNode = node.Clone(true);
+            newGraph.AddNode(clonedNode);
+            nodeMapping[node] = clonedNode;
+        }
+
+        // Re-establish connections
+        foreach (var node in Nodes)
+        {
+            var clonedNode = nodeMapping[node];
+            foreach (var input in node.InputProperties)
+            {
+                if (input.Connection != null)
+                {
+                    var connectedNode = input.Connection.Node;
+                    if (nodeMapping.TryGetValue(connectedNode as Node, out var clonedConnectedNode))
+                    {
+                        var clonedOutput = clonedConnectedNode.OutputProperties.FirstOrDefault(o =>
+                            o.InternalPropertyName == input.Connection.InternalPropertyName);
+                        var clonedInput = clonedNode.InputProperties.FirstOrDefault(i =>
+                            i.InternalPropertyName == input.InternalPropertyName);
+                        if (clonedOutput != null && clonedInput != null)
+                        {
+                            clonedOutput.ConnectTo(clonedInput);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Set custom output node if applicable
+        if (CustomOutputNode != null && nodeMapping.TryGetValue(CustomOutputNode, out var mappedOutputNode))
+        {
+            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)
     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);
     void IReadOnlyNodeGraph.AddNode(IReadOnlyNode node) => AddNode((Node)node);
@@ -81,40 +167,91 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
 
     public bool TryTraverse(Action<IReadOnlyNode> action)
     public bool TryTraverse(Action<IReadOnlyNode> action)
     {
     {
-        if(OutputNode == null) return false;
-        
-        var queue = CalculateExecutionQueueInternal(OutputNode);
-        
+        return TryTraverse(OutputNode, action);
+    }
+
+    public bool TryTraverse(IReadOnlyNode end, Action<IReadOnlyNode> action)
+    {
+        if (end == null) return false;
+
+        var queue = CalculateExecutionQueueInternal(end);
+
         foreach (var node in queue)
         foreach (var node in queue)
         {
         {
             action(node);
             action(node);
         }
         }
-        
+
         return true;
         return true;
     }
     }
 
 
+
     public void Execute(RenderContext context)
     public void Execute(RenderContext context)
     {
     {
-        if (OutputNode == null) return;
-        if(!CanExecute()) return;
+        Execute(OutputNode, context);
+    }
 
 
-        var queue = CalculateExecutionQueueInternal(OutputNode);
-        
-        foreach (var node in queue)
+    public void Execute(IEnumerable<IReadOnlyNode> nodes, RenderContext context)
+    {
+        isExecuting = true;
+        if (!CanExecute()) return;
+
+        HashSet<IReadOnlyNode> executedNodes = new();
+        foreach (var exposeVariableNode in nodes)
         {
         {
-            if (node is Node typedNode)
+            var queue = CalculateExecutionQueueInternal(exposeVariableNode);
+
+            foreach (var node in queue)
             {
             {
-                if(typedNode.IsDisposed) continue;
-                
-                typedNode.ExecuteInternal(context);
+                if (!executedNodes.Add(node)) continue;
+
+                lock (node)
+                {
+                    if (node is Node typedNode)
+                    {
+                        if (typedNode.IsDisposed) continue;
+
+                        typedNode.ExecuteInternal(context);
+                    }
+                    else
+                    {
+                        node.Execute(context);
+                    }
+                }
             }
             }
-            else
+        }
+
+        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)
+        {
+            lock (node)
             {
             {
-                node.Execute(context);
+                if (node is Node typedNode)
+                {
+                    if (typedNode.IsDisposed) continue;
+
+                    typedNode.ExecuteInternal(context);
+                }
+                else
+                {
+                    node.Execute(context);
+                }
             }
             }
         }
         }
+
+        isExecuting = false;
     }
     }
-    
+
     private bool CanExecute()
     private bool CanExecute()
     {
     {
         foreach (var node in Nodes)
         foreach (var node in Nodes)
@@ -127,7 +264,7 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
 
 
         return true;
         return true;
     }
     }
-    
+
     private void ResetCache()
     private void ResetCache()
     {
     {
         cachedExecutionList = null;
         cachedExecutionList = null;
@@ -136,9 +273,12 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
     public int GetCacheHash()
     public int GetCacheHash()
     {
     {
         HashCode hash = new();
         HashCode hash = new();
-        foreach (var node in Nodes)
+        var queue = CalculateExecutionQueueInternal(OutputNode);
+
+        foreach (var node in queue)
         {
         {
-            hash.Add(node.GetCacheHash());
+            int nodeCache = node.GetCacheHash();
+            hash.Add(nodeCache);
         }
         }
 
 
         return hash.ToHashCode();
         return hash.ToHashCode();

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

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

@@ -0,0 +1,283 @@
+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.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 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<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();
+
+    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;
+
+    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>("StampBlendMode", "STAMP_BLEND_MODE",
+            Drawie.Backend.Core.Surfaces.BlendMode.SrcOver);
+        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);
+    }
+
+    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();
+            }
+        }
+
+        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;
+        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));
+        }
+
+        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;
+        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;
+
+            previewEngine.ExecuteBrush(target,
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true },
+                [(VecI)pos], context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
+                new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(),
+                new EditorData(Colors.White, Colors.Black));
+
+            offset += 1;
+        }
+    }
+
+    public IEnumerable<float> DrawStrokePreviewEnumerable(ChunkyImage target, RenderContext context, int maxSize, VecD shift = default)
+    {
+        if (previewVectorPath == null)
+        {
+            previewVectorPath = VectorPath.FromSvgPath(PreviewSvg);
+        }
+
+        float offset = 0;
+        float pressure;
+        VecD pos;
+        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;
+
+            previewEngine.ExecuteBrush(target,
+                new BrushData(context.Graph, Id) { StrokeWidth = maxSize, AntiAliasing = true },
+                [(VecI)pos], context.FrameTime, context.ProcessingColorSpace, context.DesiredSamplingOptions,
+                new PointerInfo(pos, pressure, 0, VecD.Zero, vec4D.ZW),
+                new KeyboardInfo(),
+                new EditorData(Colors.White, Colors.Black));
+
+            offset += 1;
+            yield return offset;
+        }
+    }
+
+    public void DrawPointPreview(ChunkyImage img, RenderContext context, int size, VecD pos)
+    {
+        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();
+    }
+}

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

@@ -6,5 +6,6 @@ public enum CacheTriggerFlags
     None = 0,
     None = 0,
     Inputs = 1,
     Inputs = 1,
     Timeline = 2,
     Timeline = 2,
-    All = Inputs | Timeline
+    RenderSize = 4,
+    All = Inputs | Timeline | RenderSize
 }
 }

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

@@ -39,52 +39,53 @@ 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)
         if (Red.Value is { } red)
         {
         {
             _screenPaint.ColorFilter = _redFilter;
             _screenPaint.ColorFilter = _redFilter;
             
             
-            int savedRed = surface.Canvas.SaveLayer(_screenPaint);
+            int savedRed = surface.SaveLayer(_screenPaint);
             red.Paint(context, surface);
             red.Paint(context, surface);
             
             
-            surface.Canvas.RestoreToCount(savedRed);
+            surface.RestoreToCount(savedRed);
         }
         }
 
 
         if (Green.Value is { } green)
         if (Green.Value is { } green)
         {
         {
             _screenPaint.ColorFilter = _greenFilter;
             _screenPaint.ColorFilter = _greenFilter;
-            int savedGreen = surface.Canvas.SaveLayer(_screenPaint);
+            int savedGreen = surface.SaveLayer(_screenPaint);
             green.Paint(context, surface);
             green.Paint(context, surface);
             
             
-            surface.Canvas.RestoreToCount(savedGreen);
+            surface.RestoreToCount(savedGreen);
         }
         }
 
 
         if (Blue.Value is { } blue)
         if (Blue.Value is { } blue)
         {
         {
             _screenPaint.ColorFilter = _blueFilter;
             _screenPaint.ColorFilter = _blueFilter;
-            int savedBlue = surface.Canvas.SaveLayer(_screenPaint);
+            int savedBlue = surface.SaveLayer(_screenPaint);
             blue.Paint(context, surface);
             blue.Paint(context, surface);
             
             
-            surface.Canvas.RestoreToCount(savedBlue);
+            surface.RestoreToCount(savedBlue);
         }
         }
 
 
         if (Alpha.Value is { } alpha)
         if (Alpha.Value is { } alpha)
         {
         {
             _clearPaint.ColorFilter = Grayscale.Value ? Filters.AlphaGrayscaleFilter : null;
             _clearPaint.ColorFilter = Grayscale.Value ? Filters.AlphaGrayscaleFilter : null;
-            int savedAlpha = surface.Canvas.SaveLayer(_clearPaint);
+            int savedAlpha = surface.SaveLayer(_clearPaint);
             alpha.Paint(context, surface);
             alpha.Paint(context, surface);
             
             
-            surface.Canvas.RestoreToCount(savedAlpha);
+            surface.RestoreToCount(savedAlpha);
         }
         }
             
             
-        surface.Canvas.RestoreToCount(saved);
+        surface.RestoreToCount(saved);
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
     {
-        RectD? redBounds = PreviewUtils.FindPreviewBounds(Red.Connection, frame, elementToRenderName);
+        int frame = ctx.FrameTime.Frame;
+        /*RectD? redBounds = PreviewUtils.FindPreviewBounds(Red.Connection, frame, elementToRenderName);
         RectD? greenBounds = PreviewUtils.FindPreviewBounds(Green.Connection, frame, elementToRenderName);
         RectD? greenBounds = PreviewUtils.FindPreviewBounds(Green.Connection, frame, elementToRenderName);
         RectD? blueBounds = PreviewUtils.FindPreviewBounds(Blue.Connection, frame, elementToRenderName);
         RectD? blueBounds = PreviewUtils.FindPreviewBounds(Blue.Connection, frame, elementToRenderName);
         RectD? alphaBounds = PreviewUtils.FindPreviewBounds(Alpha.Connection, frame, elementToRenderName);
         RectD? alphaBounds = PreviewUtils.FindPreviewBounds(Alpha.Connection, frame, elementToRenderName);
@@ -116,19 +117,18 @@ public class CombineChannelsNode : RenderNode
             finalBounds = finalBounds?.Union(alphaBounds.Value) ?? alphaBounds.Value;
             finalBounds = finalBounds?.Union(alphaBounds.Value) ?? alphaBounds.Value;
         }
         }
         
         
-        return finalBounds;
+        return finalBounds;*/
+        return null;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
     {
-        if (Red.Value == null && Green.Value == null && Blue.Value == null && Alpha.Value == null)
-        {
-            return false;
-        }
+        return Red.Value != null || Green.Value != null || Blue.Value != null || Alpha.Value != null;
+    }
 
 
-        OnPaint(context, renderOn); 
-        
-        return true;
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        OnPaint(context, renderOn.Canvas);
     }
     }
 
 
     public override Node CreateCopy() => new CombineChannelsNode();
     public override Node CreateCopy() => new CombineChannelsNode();

+ 21 - 20
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineSeparate/SeparateChannelsNode.cs

@@ -10,7 +10,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
 
 
 [NodeInfo("SeparateChannels")]
 [NodeInfo("SeparateChannels")]
-public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
+public class SeparateChannelsNode : Node, IRenderInput
 {
 {
     private readonly Paint _paint = new();
     private readonly Paint _paint = new();
     
     
@@ -47,27 +47,27 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
         Grayscale = CreateInput(nameof(Grayscale), "GRAYSCALE", false);
         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);
         Paint(context, drawingSurface, _redFilter, _redGrayscaleFilter);
     }
     }
     
     
-    private void PaintGreen(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintGreen(RenderContext context, Canvas drawingSurface)
     {
     {
         Paint(context, drawingSurface, _greenFilter, _greenGrayscaleFilter);
         Paint(context, drawingSurface, _greenFilter, _greenGrayscaleFilter);
     }
     }
     
     
-    private void PaintBlue(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintBlue(RenderContext context, Canvas drawingSurface)
     {
     {
         Paint(context, drawingSurface, _blueFilter, _blueGrayscaleFilter);
         Paint(context, drawingSurface, _blueFilter, _blueGrayscaleFilter);
     }
     }
     
     
-    private void PaintAlpha(RenderContext context, DrawingSurface drawingSurface)
+    private void PaintAlpha(RenderContext context, Canvas drawingSurface)
     {
     {
         Paint(context, drawingSurface, _alphaFilter, _alphaGrayscaleFilter);
         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)
         if(Image.Value == null)
             return;
             return;
@@ -77,11 +77,11 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
         ColorFilter filter = grayscale ? grayscaleFilter : colorFilter; 
         ColorFilter filter = grayscale ? grayscaleFilter : colorFilter; 
         _paint.ColorFilter = filter;
         _paint.ColorFilter = filter;
         
         
-        int saved = drawingSurface.Canvas.SaveLayer(_paint);
+        int saved = drawingSurface.SaveLayer(_paint);
         
         
         Image.Value.Paint(context, drawingSurface);
         Image.Value.Paint(context, drawingSurface);
         
         
-        drawingSurface.Canvas.RestoreToCount(saved);
+        drawingSurface.RestoreToCount(saved);
     }
     }
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
@@ -94,13 +94,14 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
 
 
     public override Node CreateCopy() => new SeparateChannelsNode();
     public override Node CreateCopy() => new SeparateChannelsNode();
     RenderInputProperty IRenderInput.Background => Image;
     RenderInputProperty IRenderInput.Background => Image;
+    
+    // TODO: Add previews
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
     {
-        RectD? bounds = PreviewUtils.FindPreviewBounds(Image.Connection, frame, elementToRenderName);
-        return bounds;
+        return null;
     }
     }
 
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public bool RenderPreview(Canvas renderOn, RenderContext context, string elementToRenderName)
     {
     {
         if (Image.Value == null)
         if (Image.Value == null)
             return false;
             return false;
@@ -110,7 +111,7 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
         if (bounds == null)
         if (bounds == null)
             return false;
             return false;
         
         
-        renderOn.Canvas.Save();
+        renderOn.Save();
 
 
         _paint.ColorFilter = Grayscale.Value ? _redGrayscaleFilter : _redFilter;
         _paint.ColorFilter = Grayscale.Value ? _redGrayscaleFilter : _redFilter;
         RectD localBounds = new(bounds.Value.X, bounds.Value.Y, bounds.Value.Width / 2, bounds.Value.Height / 2);
         RectD localBounds = new(bounds.Value.X, bounds.Value.Y, bounds.Value.Width / 2, bounds.Value.Height / 2);
@@ -128,23 +129,23 @@ public class SeparateChannelsNode : Node, IRenderInput, IPreviewRenderable
         localBounds = new(bounds.Value.X + bounds.Value.Width / 2, bounds.Value.Y + bounds.Value.Height / 2, bounds.Value.Width / 2, bounds.Value.Height / 2);
         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);
         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;
         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);
         Image.Value.Paint(context, renderOn);
         
         
-        renderOn.Canvas.RestoreToCount(saved);
+        renderOn.RestoreToCount(saved);
     }
     }
 }
 }

+ 38 - 42
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -13,7 +13,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
 [NodeInfo("CreateImage")]
 [NodeInfo("CreateImage")]
-public class CreateImageNode : Node, IPreviewRenderable
+public class CreateImageNode : Node
 {
 {
     public OutputProperty<Texture> Output { get; }
     public OutputProperty<Texture> Output { get; }
 
 
@@ -30,6 +30,9 @@ public class CreateImageNode : Node, IPreviewRenderable
 
 
     private TextureCache textureCache = new();
     private TextureCache textureCache = new();
 
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public CreateImageNode()
     public CreateImageNode()
     {
     {
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
         Output = CreateOutput<Texture>(nameof(Output), "IMAGE", null);
@@ -48,6 +51,7 @@ public class CreateImageNode : Node, IPreviewRenderable
         }
         }
 
 
         var surface = Render(context);
         var surface = Render(context);
+        RenderPreviews(surface, context);
 
 
         Output.Value = surface;
         Output.Value = surface;
 
 
@@ -56,7 +60,8 @@ public class CreateImageNode : Node, IPreviewRenderable
 
 
     private Texture Render(RenderContext context)
     private Texture Render(RenderContext context)
     {
     {
-        var surface = textureCache.RequestTexture(0, (VecI)(Size.Value * context.ChunkResolution.Multiplier()), context.ProcessingColorSpace, false);
+        int id = (Size.Value * context.ChunkResolution.Multiplier()).GetHashCode();
+        var surface = textureCache.RequestTexture(id, (VecI)(Size.Value * context.ChunkResolution.Multiplier()), context.ProcessingColorSpace, false);
         surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
         surface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
 
 
         if (Fill.Value is ColorPaintable colorPaintable)
         if (Fill.Value is ColorPaintable colorPaintable)
@@ -67,13 +72,14 @@ public class CreateImageNode : Node, IPreviewRenderable
         {
         {
             using Paint paint = new Paint();
             using Paint paint = new Paint();
             paint.SetPaintable(Fill.Value);
             paint.SetPaintable(Fill.Value);
+            paint.BlendMode = BlendMode.Src;
             surface.DrawingSurface.Canvas.DrawRect(0, 0, Size.Value.X, Size.Value.Y, paint);
             surface.DrawingSurface.Canvas.DrawRect(0, 0, Size.Value.X, Size.Value.Y, paint);
         }
         }
 
 
         int saved = surface.DrawingSurface.Canvas.Save();
         int saved = surface.DrawingSurface.Canvas.Save();
 
 
         RenderContext ctx = context.Clone();
         RenderContext ctx = context.Clone();
-        ctx.RenderSurface = surface.DrawingSurface;
+        ctx.RenderSurface = surface.DrawingSurface.Canvas;
         ctx.RenderOutputSize = surface.Size;
         ctx.RenderOutputSize = surface.Size;
 
 
         float chunkMultiplier = (float)context.ChunkResolution.Multiplier();
         float chunkMultiplier = (float)context.ChunkResolution.Multiplier();
@@ -82,62 +88,52 @@ public class CreateImageNode : Node, IPreviewRenderable
             surface.DrawingSurface.Canvas.TotalMatrix.Concat(
             surface.DrawingSurface.Canvas.TotalMatrix.Concat(
                 Matrix3X3.CreateScale(chunkMultiplier, chunkMultiplier).Concat(ContentMatrix.Value)));
                 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);
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
         return surface;
         return surface;
     }
     }
 
 
-    private void OnPaint(RenderContext context, DrawingSurface surface)
+    private void OnPaint(RenderContext context, Canvas surface)
     {
     {
         if(Output.Value == null || Output.Value.IsDisposed) return;
         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);
-
-        surface.Canvas.RestoreToCount(saved);
-    }
+        int saved = surface.Save();
+        surface.Scale((float)context.ChunkResolution.InvertedMultiplier());
+        surface.DrawSurface(Output.Value.DrawingSurface, 0, 0);
 
 
-    public override Node CreateCopy() => new CreateImageNode();
-
-    public override void Dispose()
-    {
-        base.Dispose();
-        textureCache.Dispose();
+        surface.RestoreToCount(saved);
     }
     }
 
 
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    private void RenderPreviews(Texture surface, RenderContext context)
     {
     {
-        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
         {
-            return null;
-        }
+            var texture = request.Texture;
+            if (texture is null) continue;
 
 
-        return new RectD(0, 0, Size.Value.X, Size.Value.Y);
-    }
+            int saved = texture.DrawingSurface.Canvas.Save();
 
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Size.Value.X <= 0 || Size.Value.Y <= 0)
-        {
-            return false;
-        }
+            VecD scaling = PreviewUtility.CalculateUniformScaling(surface.Size, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(surface.Size, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
 
 
-        if (Output.Value == null)
-        {
-            return false;
+            texture.DrawingSurface.Canvas.Clear();
+            texture.DrawingSurface.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
         }
         }
+    }
 
 
-        var surface = Render(context);
-        
-        if (surface == null || surface.IsDisposed)
-        {
-            return false;
-        }
-        
-        renderOn.Canvas.DrawSurface(surface.DrawingSurface, 0, 0);
-        
-        return true;
+    public override Node CreateCopy() => new CreateImageNode();
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        textureCache.Dispose();
     }
     }
 }
 }

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

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

@@ -32,7 +32,6 @@ public class OutlineNode : RenderNode, IRenderInput
     private ImageFilter filter;
     private ImageFilter filter;
 
 
     private OutlineType? lastType = null;
     private OutlineType? lastType = null;
-    private VecI lastDocumentSize;
 
 
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override bool ExecuteOnlyOnCacheChange => true;
 
 
@@ -52,7 +51,6 @@ public class OutlineNode : RenderNode, IRenderInput
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {
         base.OnExecute(context);
         base.OnExecute(context);
-        lastDocumentSize = context.RenderOutputSize;
 
 
         Kernel finalKernel = Type.Value switch
         Kernel finalKernel = Type.Value switch
         {
         {
@@ -71,7 +69,7 @@ public class OutlineNode : RenderNode, IRenderInput
         lastType = Type.Value;
         lastType = Type.Value;
     }
     }
 
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
     {
         if (Background.Value == null)
         if (Background.Value == null)
         {
         {
@@ -91,7 +89,7 @@ public class OutlineNode : RenderNode, IRenderInput
             bool isAdjusted = context.DocumentSize == context.RenderOutputSize;
             bool isAdjusted = context.DocumentSize == context.RenderOutputSize;
             ctx.RenderOutputSize = isAdjusted ? context.RenderOutputSize : (VecI)(context.RenderOutputSize * context.ChunkResolution.InvertedMultiplier());
             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);
             temp.DrawingSurface.Canvas.RestoreToCount(saved);
 
 
@@ -108,28 +106,27 @@ public class OutlineNode : RenderNode, IRenderInput
                 temp.DrawingSurface.Canvas.RestoreToCount(saved);
                 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);
         Background?.Value?.Paint(context, surface);
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
     {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+        return new RectD(0, 0, ctx.DocumentSize.X, ctx.DocumentSize.Y);
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
         int saved = renderOn.Canvas.Save();
         int saved = renderOn.Canvas.Save();
         renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
         renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
         renderOn.Canvas.RestoreToCount(saved);
         renderOn.Canvas.RestoreToCount(saved);
-        return true;
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

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

@@ -92,7 +92,7 @@ public class PosterizationNode : RenderNode, IRenderInput
         shader = Shader.Create(shaderCode, uniforms, out _);
         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)
         if (Background.Value == null)
         {
         {
@@ -117,7 +117,7 @@ public class PosterizationNode : RenderNode, IRenderInput
         }
         }
         
         
         using Texture temp = Texture.ForProcessing(surface, colorSpace);
         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();
         var snapshot = temp.DrawingSurface.Snapshot();
         
         
         lastImageShader?.Dispose();
         lastImageShader?.Dispose();
@@ -136,21 +136,10 @@ public class PosterizationNode : RenderNode, IRenderInput
         temp.DrawingSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
         temp.DrawingSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
         temp.DrawingSurface.Canvas.RestoreToCount(savedTemp);
         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);
-    }
-    
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        OnPaint(context, renderOn);
-        return true;
+        var saved = surface.Save();
+        surface.SetMatrix(Matrix3X3.Identity);
+        surface.DrawSurface(temp.DrawingSurface, 0, 0);
+        surface.RestoreToCount(saved);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

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

@@ -28,6 +28,9 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
     
     
     public InputProperty<bool> InvertMask { get; }
     public InputProperty<bool> InvertMask { get; }
 
 
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
     public ApplyFilterNode()
     public ApplyFilterNode()
     {
     {
         Background = CreateRenderInput("Input", "IMAGE");
         Background = CreateRenderInput("Input", "IMAGE");
@@ -38,13 +41,13 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         AllowHighDpiRendering = true;
         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;
+        AllowHighDpiRendering = (Background.Connection?.Node as RenderNode)?.AllowHighDpiRendering ?? true;
         base.Paint(context, surface);
         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);
         using var _ = DetermineTargetSurface(context, outputSurface, out var processingSurface);
 
 
@@ -57,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);
         _paint.SetFilters(Filter.Value);
 
 
@@ -67,16 +70,16 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
             return;
             return;
         }
         }
 
 
-        var layer = processingSurface.Canvas.SaveLayer(_paint);
+        var layer = processingSurface.SaveLayer(_paint);
         Background.Value?.Paint(context, processingSurface);
         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);
         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());
         using var srgbSurface = Texture.ForProcessing(intermediate.Size, ColorSpace.CreateSrgb());
 
 
@@ -84,14 +87,14 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         srgbSurface.DrawingSurface.Canvas.DrawSurface(intermediate.DrawingSurface, 0, 0);
         srgbSurface.DrawingSurface.Canvas.DrawSurface(intermediate.DrawingSurface, 0, 0);
         srgbSurface.DrawingSurface.Canvas.Restore();
         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;
         targetSurface = outputSurface;
         
         
@@ -100,27 +103,30 @@ public sealed class ApplyFilterNode : RenderNode, IRenderInput
         
         
         Background.Value?.Paint(context, outputSurface);
         Background.Value?.Paint(context, outputSurface);
         var texture = Texture.ForProcessing(outputSurface, context.ProcessingColorSpace);
         var texture = Texture.ForProcessing(outputSurface, context.ProcessingColorSpace);
-        targetSurface = texture.DrawingSurface;
+        targetSurface = texture.DrawingSurface.Canvas;
         
         
         return texture;
         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;
         _maskPaint.BlendMode = !InvertMask.Value ? BlendMode.DstIn : BlendMode.DstOut;
-        var maskLayer = processedSurface.Canvas.SaveLayer(_maskPaint);
+        var maskLayer = processedSurface.SaveLayer(_maskPaint);
         Mask.Value?.Paint(context, processedSurface);
         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(int frame, string elementToRenderName = "") =>
-        PreviewUtils.FindPreviewBounds(Background.Connection, frame, elementToRenderName);
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "") =>
+        null;
+        /*
+        PreviewUtils.FindPreviewBounds(Background.Connection, ctx.FrameTime.Frame, elementToRenderName);
+        */
 
 
     public override Node CreateCopy() => new ApplyFilterNode();
     public override Node CreateCopy() => new ApplyFilterNode();
 
 

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

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

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

@@ -44,6 +44,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
 
 
     public override void Render(SceneObjectRenderContext sceneContext)
     public override void Render(SceneObjectRenderContext sceneContext)
     {
     {
+        RenderPreviews(sceneContext);
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
         {
             Output.Value = Background.Value;
             Output.Value = Background.Value;
@@ -61,10 +62,10 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
                 paint.ImageFilter = Filters.Value?.ImageFilter;
                 paint.ImageFilter = Filters.Value?.ImageFilter;
             }
             }
 
 
-            int saved = sceneContext.RenderSurface.Canvas.SaveLayer(paint);
+            int saved = sceneContext.RenderSurface.SaveLayer(paint);
             Content.Value?.Paint(sceneContext, sceneContext.RenderSurface);
             Content.Value?.Paint(sceneContext, sceneContext.RenderSurface);
 
 
-            sceneContext.RenderSurface.Canvas.RestoreToCount(saved);
+            sceneContext.RenderSurface.RestoreToCount(saved);
             return;
             return;
         }
         }
 
 
@@ -89,17 +90,22 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         VecI size = sceneContext.RenderSurface.DeviceClipBounds.Size + sceneContext.RenderSurface.DeviceClipBounds.Pos;
         VecI size = sceneContext.RenderSurface.DeviceClipBounds.Size + sceneContext.RenderSurface.DeviceClipBounds.Pos;
         var outputWorkingSurface = RequestTexture(0, size, sceneContext.ProcessingColorSpace, true);
         var outputWorkingSurface = RequestTexture(0, size, sceneContext.ProcessingColorSpace, true);
         outputWorkingSurface.DrawingSurface.Canvas.Save();
         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.ImageFilter = null;
         blendPaint.ColorFilter = null;
         blendPaint.ColorFilter = null;
 
 
-        Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface);
+        Content.Value?.Paint(sceneContext, outputWorkingSurface.DrawingSurface.Canvas);
 
 
-        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface, sceneContext, sceneContext.ChunkResolution);
+        int saved2 = outputWorkingSurface.DrawingSurface.Canvas.Save();
+        outputWorkingSurface.DrawingSurface.Canvas.Scale((float)sceneContext.ChunkResolution.InvertedMultiplier());
+
+        ApplyMaskIfPresent(outputWorkingSurface.DrawingSurface.Canvas, sceneContext, sceneContext.ChunkResolution);
+
+        outputWorkingSurface.DrawingSurface.Canvas.RestoreToCount(saved2);
 
 
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         if (Background.Value != null && sceneContext.TargetPropertyOutput != RawOutput)
         {
         {
@@ -110,7 +116,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
             outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             outputWorkingSurface.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
             if (Background.Connection.Node is IClipSource clipSource && ClipToPreviousMember)
             if (Background.Connection.Node is IClipSource clipSource && ClipToPreviousMember)
             {
             {
-                DrawClipSource(tempSurface.DrawingSurface, clipSource, sceneContext);
+                DrawClipSource(tempSurface.DrawingSurface.Canvas, clipSource, sceneContext);
             }
             }
 
 
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
@@ -119,9 +125,9 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         AdjustPaint(useFilters);
         AdjustPaint(useFilters);
 
 
         blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
         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();
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
     }
 
 
@@ -274,47 +280,40 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         return guids;
         return guids;
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
     {
-        if (elementFor == nameof(EmbeddedMask))
+        if (elementToRenderName == nameof(EmbeddedMask))
         {
         {
-            return base.GetPreviewBounds(frame, elementFor);
+            return base.ShouldRenderPreview(elementToRenderName);
         }
         }
 
 
-        return GetApproxBounds(frame);
+        return Content.Connection != null;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        return GetApproxBounds(ctx.FrameTime);
+    }
+
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
         string elementToRenderName)
     {
     {
         if (elementToRenderName == nameof(EmbeddedMask))
         if (elementToRenderName == nameof(EmbeddedMask))
         {
         {
-            return base.RenderPreview(renderOn, context, elementToRenderName);
+            base.RenderPreview(renderOn, context, elementToRenderName);
+            return;
         }
         }
 
 
         if (Content.Connection != null)
         if (Content.Connection != null)
         {
         {
-            var executionQueue = GraphUtils.CalculateExecutionQueue(Content.Connection.Node, FilterInvisibleFolders);
-            while (executionQueue.Count > 0)
+            if (context is SceneObjectRenderContext ctx)
             {
             {
-                IReadOnlyNode node = executionQueue.Dequeue();
-
-                if (node is IReadOnlyStructureNode { IsVisible.Value: false })
-                {
-                    continue;
-                }
-
-                if (node is IPreviewRenderable previewRenderable)
-                {
-                    previewRenderable.RenderPreview(renderOn, context, elementToRenderName);
-                }
+                RenderFolderContent(ctx, true);
             }
             }
         }
         }
-
-        return true;
     }
     }
 
 
-    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto)
     {
     {
         if (Content.Connection != null)
         if (Content.Connection != null)
         {
         {
@@ -330,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;
         Output.FirstInChain = null;
     }
     }
 
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
     {
         if (Background.Value == null)
         if (Background.Value == null)
         {
         {
@@ -43,9 +43,9 @@ public sealed class MaskNode : RenderNode, IRenderInput
 
 
         maskPaint.BlendMode = !Invert.Value ? BlendMode.DstIn : BlendMode.DstOut;
         maskPaint.BlendMode = !Invert.Value ? BlendMode.DstIn : BlendMode.DstOut;
 
 
-        int layer = surface.Canvas.SaveLayer(maskPaint);
+        int layer = surface.SaveLayer(maskPaint);
         Mask.Value.Paint(context, surface);
         Mask.Value.Paint(context, surface);
-        surface.Canvas.RestoreToCount(layer);
+        surface.RestoreToCount(layer);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

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

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Helpers;
@@ -28,10 +29,9 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
     private VecI startSize;
     private VecI startSize;
     private ColorSpace colorSpace;
     private ColorSpace colorSpace;
-    private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
 
-    private Texture fullResrenderedSurface;
-    private int renderedSurfaceFrame = -1;
+
+    private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
 
     public ImageLayerNode(VecI size, ColorSpace colorSpace)
     public ImageLayerNode(VecI size, ColorSpace colorSpace)
     {
     {
@@ -87,78 +87,82 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     }
     }
 
 
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
     protected internal override void DrawLayerInScene(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         bool useFilters = true)
         bool useFilters = true)
     {
     {
-        int scaled = workingSurface.Canvas.Save();
+        int scaled = workingSurface.Save();
         float multiplier = (float)ctx.ChunkResolution.InvertedMultiplier();
         float multiplier = (float)ctx.ChunkResolution.InvertedMultiplier();
-        workingSurface.Canvas.Translate(GetScenePosition(ctx.FrameTime));
+        workingSurface.Translate(GetScenePosition(ctx.FrameTime));
 
 
         base.DrawLayerInScene(ctx, workingSurface, useFilters);
         base.DrawLayerInScene(ctx, workingSurface, useFilters);
 
 
-        workingSurface.Canvas.RestoreToCount(scaled);
+        workingSurface.RestoreToCount(scaled);
     }
     }
 
 
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
     protected internal override void DrawLayerOnTexture(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         ChunkResolution resolution,
         ChunkResolution resolution,
         bool useFilters, Paint paint)
         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);
         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)
         Paint paint)
     {
     {
-        DrawLayer(workingSurface, paint, ctx);
+        DrawLayer(workingSurface, paint, ctx, false);
     }
     }
 
 
-    protected override void DrawWithFilters(SceneObjectRenderContext context, DrawingSurface workingSurface,
+    protected override void DrawWithFilters(SceneObjectRenderContext context, Canvas workingSurface,
         Paint paint)
         Paint paint)
     {
     {
-        DrawLayer(workingSurface, paint, context);
+        DrawLayer(workingSurface, paint, context, true);
     }
     }
 
 
-    private void DrawLayer(DrawingSurface workingSurface, Paint paint, SceneObjectRenderContext ctx)
+    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);
         var sceneSize = GetSceneSize(ctx.FrameTime);
-        VecD topLeft = sceneSize / 2f;
+        RectI latestSize = new(0, 0, layerImage.LatestSize.X, layerImage.LatestSize.Y);
+        var region = ctx.VisibleDocumentRegion ?? latestSize;
+
+        VecD topLeft = region.TopLeft - sceneSize / 2;
 
 
-        if (renderedSurfaceFrame == null || ctx.FullRerender || ctx.FrameTime.Frame != renderedSurfaceFrame)
+        topLeft *= ctx.ChunkResolution.Multiplier();
+        workingSurface.Scale((float)ctx.ChunkResolution.InvertedMultiplier());
+        var img = GetLayerImageAtFrame(ctx.FrameTime.Frame);
+
+        if (saveLayer)
         {
         {
-            GetLayerImageAtFrame(ctx.FrameTime.Frame).DrawMostUpToDateRegionOn(
-                new RectI(0, 0, layerImage.LatestSize.X, layerImage.LatestSize.Y),
-                ChunkResolution.Full,
-                workingSurface, -topLeft, paint);
+            workingSurface.SaveLayer(paint);
+        }
+
+        if (!ctx.FullRerender)
+        {
+            img.DrawMostUpToDateRegionOnWithAffected(
+                region,
+                ctx.ChunkResolution,
+                workingSurface, ctx.AffectedArea, topLeft, saveLayer ? null : paint, ctx.DesiredSamplingOptions);
         }
         }
         else
         else
         {
         {
-            if (ctx.DesiredSamplingOptions == SamplingOptions.Default)
-            {
-                workingSurface.Canvas.DrawSurface(
-                    fullResrenderedSurface.DrawingSurface, -(float)topLeft.X, -(float)topLeft.Y, paint);
-            }
-            else
-            {
-                using var snapshot = fullResrenderedSurface.DrawingSurface.Snapshot();
-                workingSurface.Canvas.DrawImage(snapshot, -(float)topLeft.X, -(float)topLeft.Y,
-                    ctx.DesiredSamplingOptions,
-                    paint);
-            }
+            img.DrawMostUpToDateRegionOn(
+                region,
+                ctx.ChunkResolution,
+                workingSurface, topLeft, saveLayer ? null : paint, ctx.DesiredSamplingOptions);
         }
         }
 
 
-        workingSurface.Canvas.RestoreToCount(saved);
+        workingSurface.RestoreToCount(saved);
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    public override RectD? GetPreviewBounds(RenderContext context, string elementFor = "")
     {
     {
         if (IsDisposed)
         if (IsDisposed)
         {
         {
@@ -167,11 +171,16 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
         if (elementFor == nameof(EmbeddedMask))
         if (elementFor == nameof(EmbeddedMask))
         {
         {
-            return base.GetPreviewBounds(frame, elementFor);
+            return base.GetPreviewBounds(context, elementFor);
         }
         }
 
 
         if (Guid.TryParse(elementFor, out Guid guid))
         if (Guid.TryParse(elementFor, out Guid guid))
         {
         {
+            if (guid == Id)
+            {
+                return new RectD(0, 0, layerImage.CommittedSize.X, layerImage.CommittedSize.Y);
+            }
+
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
 
 
             if (keyFrame != null)
             if (keyFrame != null)
@@ -182,19 +191,13 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                     return null;
                     return null;
                 }
                 }
 
 
-                RectI? bounds = (RectI?)GetApproxBounds(kf);
-                if (bounds.HasValue)
-                {
-                    return new RectD(bounds.Value.X, bounds.Value.Y,
-                        Math.Min(bounds.Value.Width, kf.CommittedSize.X),
-                        Math.Min(bounds.Value.Height, kf.CommittedSize.Y));
-                }
+                return new RectD(0, 0, kf.CommittedSize.X, kf.CommittedSize.Y);
             }
             }
         }
         }
 
 
         try
         try
         {
         {
-            var kf = GetLayerImageAtFrame(frame);
+            var kf = GetLayerImageAtFrame(context.FrameTime.Frame);
             if (kf == null)
             if (kf == null)
             {
             {
                 return null;
                 return null;
@@ -216,8 +219,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
         }
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOnto, RenderContext context,
-        string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
     {
         if (IsDisposed)
         if (IsDisposed)
         {
         {
@@ -226,12 +228,28 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
         if (elementToRenderName == nameof(EmbeddedMask))
         if (elementToRenderName == nameof(EmbeddedMask))
         {
         {
-            return base.RenderPreview(renderOnto, context, elementToRenderName);
+            return base.ShouldRenderPreview(elementToRenderName);
+        }
+
+        return true;
+    }
+
+    public override void RenderPreview(DrawingSurface renderOnto, RenderContext context,
+        string elementToRenderName)
+    {
+        if (IsDisposed)
+        {
+            return;
+        }
+
+        if (elementToRenderName == nameof(EmbeddedMask))
+        {
+            base.RenderPreview(renderOnto, context, elementToRenderName);
+            return;
         }
         }
 
 
         var img = GetLayerImageAtFrame(context.FrameTime.Frame);
         var img = GetLayerImageAtFrame(context.FrameTime.Frame);
 
 
-        int cacheFrame = context.FrameTime.Frame;
         if (Guid.TryParse(elementToRenderName, out Guid guid))
         if (Guid.TryParse(elementToRenderName, out Guid guid))
         {
         {
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
             var keyFrame = keyFrames.FirstOrDefault(x => x.KeyFrameGuid == guid);
@@ -239,46 +257,27 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             if (keyFrame != null)
             if (keyFrame != null)
             {
             {
                 img = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
                 img = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
-                cacheFrame = keyFrame.StartFrame;
             }
             }
             else if (guid == Id)
             else if (guid == Id)
             {
             {
                 img = GetLayerImageAtFrame(0);
                 img = GetLayerImageAtFrame(0);
-                cacheFrame = 0;
             }
             }
         }
         }
 
 
         if (img is null)
         if (img is null)
         {
         {
-            return false;
+            return;
         }
         }
 
 
-        if (renderedSurfaceFrame == cacheFrame)
-        {
-            int saved = renderOnto.Canvas.Save();
-            renderOnto.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-            if (context.DesiredSamplingOptions == SamplingOptions.Default)
-            {
-                renderOnto.Canvas.DrawSurface(
-                    fullResrenderedSurface.DrawingSurface, 0, 0, blendPaint);
-            }
-            else
-            {
-                using var snapshot = fullResrenderedSurface.DrawingSurface.Snapshot();
-                renderOnto.Canvas.DrawImage(snapshot, 0, 0, context.DesiredSamplingOptions, blendPaint);
-            }
+        int saved = renderOnto.Canvas.Save();
+        renderOnto.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
 
 
-            renderOnto.Canvas.RestoreToCount(saved);
-        }
-        else
-        {
-            img.DrawMostUpToDateRegionOn(
-                new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
-                context.ChunkResolution,
-                renderOnto, VecI.Zero, blendPaint, context.DesiredSamplingOptions);
-        }
+        img.DrawCommittedRegionOn(
+            new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
+            context.ChunkResolution,
+            renderOnto.Canvas, VecI.Zero, replacePaint, context.DesiredSamplingOptions);
 
 
-        return true;
+        renderOnto.Canvas.RestoreToCount(saved);
     }
     }
 
 
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
@@ -330,12 +329,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         return image;
         return image;
     }
     }
 
 
-    public override void Dispose()
-    {
-        base.Dispose();
-        fullResrenderedSurface?.Dispose();
-    }
-
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
 
 
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageByKeyFrameGuid(Guid keyFrameGuid) =>
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageByKeyFrameGuid(Guid keyFrameGuid) =>
@@ -346,17 +339,6 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
 
 
-    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
-        ColorSpace processColorSpace)
-    {
-        base.RenderChunk(chunkPos, resolution, frameTime, processColorSpace);
-
-        var img = GetLayerImageAtFrame(frameTime.Frame);
-
-        RenderChunkyImageChunk(chunkPos, resolution, img, 85, processColorSpace, ref fullResrenderedSurface);
-        renderedSurfaceFrame = frameTime.Frame;
-    }
-
     public void ForEveryFrame(Action<ChunkyImage> action)
     public void ForEveryFrame(Action<ChunkyImage> action)
     {
     {
         foreach (var frame in keyFrames)
         foreach (var frame in keyFrames)

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

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

@@ -22,6 +22,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
     public override void Render(SceneObjectRenderContext sceneContext)
     public override void Render(SceneObjectRenderContext sceneContext)
     {
     {
+        RenderPreviews(sceneContext);
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         if (!IsVisible.Value || Opacity.Value <= 0 || IsEmptyMask())
         {
         {
             Output.Value = Background.Value;
             Output.Value = Background.Value;
@@ -35,8 +36,11 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             sceneContext.TargetPropertyOutput == Output);
             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 (!HasOperations())
         {
         {
             if (Background.Value != null)
             if (Background.Value != null)
@@ -44,6 +48,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
                 blendPaint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             }
             }
 
 
+            // TODO: Optimizattion: Simple graphs can draw directly to scene, skipping the intermediate surface
             if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.RenderOutputSize)
             if (AllowHighDpiRendering || renderOnto.DeviceClipBounds.Size == context.RenderOutputSize)
             {
             {
                 DrawLayerInScene(context, renderOnto, useFilters);
                 DrawLayerInScene(context, renderOnto, useFilters);
@@ -59,7 +64,13 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
                 var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
                 var tempSurface = TryInitWorkingSurface(context.RenderOutputSize, context.ChunkResolution,
                     context.ProcessingColorSpace, 22);
                     context.ProcessingColorSpace, 22);
 
 
-                DrawLayerOnTexture(context, tempSurface.DrawingSurface, context.ChunkResolution, useFilters, targetPaint);
+                var originalSurface = context.RenderSurface;
+                context.RenderSurface = tempSurface.DrawingSurface.Canvas;
+
+                DrawLayerOnTexture(context, tempSurface.DrawingSurface.Canvas, context.ChunkResolution, useFilters,
+                    targetPaint);
+
+                context.RenderSurface = originalSurface;
 
 
                 blendPaint.SetFilters(null);
                 blendPaint.SetFilters(null);
                 DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution,
                 DrawWithResolution(tempSurface.DrawingSurface, renderOnto, context.ChunkResolution,
@@ -72,7 +83,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         VecI size = AllowHighDpiRendering
         VecI size = AllowHighDpiRendering
             ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
             ? renderOnto.DeviceClipBounds.Size + renderOnto.DeviceClipBounds.Pos
             : context.RenderOutputSize;
             : context.RenderOutputSize;
-        int saved = renderOnto.Canvas.Save();
+        int saved = renderOnto.Save();
 
 
         var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
         var adjustedResolution = AllowHighDpiRendering ? ChunkResolution.Full : context.ChunkResolution;
 
 
@@ -83,8 +94,8 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         outputWorkingSurface.DrawingSurface.Canvas.Save();
         outputWorkingSurface.DrawingSurface.Canvas.Save();
         if (AllowHighDpiRendering)
         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
         using var paint = new Paint
@@ -92,9 +103,15 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             Color = new Color(255, 255, 255, 255), BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.SrcOver
             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)
         if (Background.Value != null)
         {
         {
@@ -115,7 +132,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
             tempSurface.DrawingSurface.Canvas.Clear();
             tempSurface.DrawingSurface.Canvas.Clear();
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             if (Background.Connection is { Node: IClipSource clipSource } && ClipToPreviousMember)
             {
             {
-                DrawClipSource(tempSurface.DrawingSurface, clipSource, context);
+                DrawClipSource(tempSurface.DrawingSurface.Canvas, clipSource, context);
             }
             }
 
 
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
             ApplyRasterClip(outputWorkingSurface.DrawingSurface, tempSurface.DrawingSurface);
@@ -134,51 +151,52 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, adjustedResolution,
         DrawWithResolution(outputWorkingSurface.DrawingSurface, renderOnto, adjustedResolution,
             context.DesiredSamplingOptions);
             context.DesiredSamplingOptions);
 
 
-        renderOnto.Canvas.RestoreToCount(saved);
+        renderOnto.RestoreToCount(saved);
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
         outputWorkingSurface.DrawingSurface.Canvas.Restore();
     }
     }
 
 
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
     protected internal virtual void DrawLayerOnTexture(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         ChunkResolution resolution,
         ChunkResolution resolution,
         bool useFilters, Paint paint)
         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);
         DrawLayerOnto(ctx, workingSurface, useFilters, paint);
 
 
-        workingSurface.Canvas.RestoreToCount(scaled);
+        workingSurface.RestoreToCount(scaled);
     }
     }
 
 
-    private void DrawWithResolution(DrawingSurface source, DrawingSurface target, ChunkResolution resolution, SamplingOptions sampling)
+    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();
         float multiplier = (float)resolution.InvertedMultiplier();
-        target.Canvas.Scale(multiplier, multiplier);
+        target.Scale(multiplier, multiplier);
 
 
         if (sampling == SamplingOptions.Default)
         if (sampling == SamplingOptions.Default)
         {
         {
-            target.Canvas.DrawSurface(source, 0, 0, blendPaint);
+            target.DrawSurface(source, 0, 0, blendPaint);
         }
         }
         else
         else
         {
         {
             using var snapshot = source.Snapshot();
             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,
     protected internal virtual void DrawLayerInScene(SceneObjectRenderContext ctx,
-        DrawingSurface workingSurface,
+        Canvas workingSurface,
         bool useFilters = true)
         bool useFilters = true)
     {
     {
         DrawLayerOnto(ctx, workingSurface, useFilters, blendPaint);
         DrawLayerOnto(ctx, workingSurface, useFilters, blendPaint);
     }
     }
 
 
-    protected void DrawLayerOnto(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected void DrawLayerOnto(SceneObjectRenderContext ctx, Canvas workingSurface,
         bool useFilters, Paint paint)
         bool useFilters, Paint paint)
     {
     {
         paint.Color = paint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
         paint.Color = paint.Color.WithAlpha((byte)Math.Round(Opacity.Value * ctx.Opacity * 255));
@@ -188,13 +206,13 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         int saved = -1;
         int saved = -1;
         if (!ctx.ProcessingColorSpace.IsSrgb && useFilters && Filters.Value != null)
         if (!ctx.ProcessingColorSpace.IsSrgb && useFilters && Filters.Value != null)
         {
         {
-            saved = workingSurface.Canvas.Save();
+            saved = workingSurface.Save();
 
 
             tex = Texture.ForProcessing(workingSurface,
             tex = Texture.ForProcessing(workingSurface,
                 ColorSpace.CreateSrgb());
                 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)
         if (useFilters && Filters.Value != null)
@@ -210,16 +228,17 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
 
 
         if (targetSurface != workingSurface)
         if (targetSurface != workingSurface)
         {
         {
-            workingSurface.Canvas.DrawSurface(targetSurface, 0, 0);
+            workingSurface.DrawSurface(targetSurface.Surface, 0, 0);
             tex.Dispose();
             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);
         Paint paint);
 
 
-    protected abstract void DrawWithFilters(SceneObjectRenderContext ctx, DrawingSurface workingSurface,
+    protected abstract void DrawWithFilters(SceneObjectRenderContext ctx, Canvas workingSurface,
         Paint paint);
         Paint paint);
 
 
     protected Texture TryInitWorkingSurface(VecI imageSize, ChunkResolution resolution, ColorSpace processingCs, int id)
     protected Texture TryInitWorkingSurface(VecI imageSize, ChunkResolution resolution, ColorSpace processingCs, int id)
@@ -245,7 +264,7 @@ public abstract class LayerNode : StructureNode, IReadOnlyLayerNode, IClipSource
         return workingSurface;
         return workingSurface;
     }
     }
 
 
-    void IClipSource.DrawClipSource(SceneObjectRenderContext context, DrawingSurface drawOnto)
+    void IClipSource.DrawClipSource(SceneObjectRenderContext context, Canvas drawOnto)
     {
     {
         RenderContent(context, drawOnto, false);
         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.GreaterThanOrEqual => xConst >= yConst ? 1 : 0,
             MathNodeMode.LessThan => xConst < yConst ? 1 : 0,
             MathNodeMode.LessThan => xConst < yConst ? 1 : 0,
             MathNodeMode.LessThanOrEqual => 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.Power => Math.Pow(xConst, yConst),
             MathNodeMode.Logarithm => Math.Log(xConst, yConst),
             MathNodeMode.Logarithm => Math.Log(xConst, yConst),
             MathNodeMode.NaturalLogarithm => Math.Log(xConst),
             MathNodeMode.NaturalLogarithm => Math.Log(xConst),

+ 35 - 13
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

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

+ 11 - 38
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)
         if (Top.Value == null && Bottom.Value == null)
         {
         {
@@ -50,19 +50,19 @@ public class MergeNode : RenderNode
         Merge(target, context);
         Merge(target, context);
     }
     }
 
 
-    private void Merge(DrawingSurface target, RenderContext context)
+    private void Merge(Canvas target, RenderContext context)
     {
     {
         if (Bottom.Value != null && Top.Value != null)
         if (Bottom.Value != null && Top.Value != null)
         {
         {
-            int saved = target.Canvas.SaveLayer();
+            int saved = target.SaveLayer();
             Bottom.Value?.Paint(context, target);
             Bottom.Value?.Paint(context, target);
-            target.Canvas.RestoreToCount(saved);
+            target.RestoreToCount(saved);
 
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
-            target.Canvas.SaveLayer(paint);
+            target.SaveLayer(paint);
 
 
             Top.Value?.Paint(context, target);
             Top.Value?.Paint(context, target);
-            target.Canvas.RestoreToCount(saved);
+            target.RestoreToCount(saved);
             return;
             return;
         }
         }
 
 
@@ -70,46 +70,19 @@ public class MergeNode : RenderNode
         Top.Value?.Paint(context, target);
         Top.Value?.Paint(context, target);
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
     {
-        if (Top.Value == null && Bottom.Value == null)
-        {
-            return null;
-        }
-
-        RectD? totalBounds = null;
-
-        if (Top.Connection != null && Top.Connection.Node is IPreviewRenderable topPreview)
-        {
-            var topBounds = topPreview.GetPreviewBounds(frame, elementToRenderName);
-            if (topBounds != null)
-            {
-                totalBounds = totalBounds?.Union(topBounds.Value) ?? topBounds;
-            }
-        }
-
-        if (Bottom.Connection != null && Bottom.Connection.Node is IPreviewRenderable bottomPreview)
-        {
-            var bottomBounds = bottomPreview.GetPreviewBounds(frame, elementToRenderName);
-            if (bottomBounds != null)
-            {
-                totalBounds = totalBounds?.Union(bottomBounds.Value) ?? bottomBounds;
-            }
-        }
-
-        return totalBounds;
+        return Top.Value != null || Bottom.Value != null;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
         if (Top.Value == null && Bottom.Value == null)
         if (Top.Value == null && Bottom.Value == null)
         {
         {
-            return false;
+            return;
         }
         }
 
 
-        Merge(renderOn, context);
-
-        return true;
+        Merge(renderOn.Canvas, context);
     }
     }
 
 
     public override void Dispose()
     public override void Dispose()

+ 34 - 16
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -12,19 +12,19 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
 [NodeInfo("ModifyImageLeft")]
 [NodeInfo("ModifyImageLeft")]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
 [PairNode(typeof(ModifyImageRightNode), "ModifyImageZone", true)]
-public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
+public class ModifyImageLeftNode : Node, IPairNode
 {
 {
     public InputProperty<Texture?> Image { get; }
     public InputProperty<Texture?> Image { get; }
 
 
     public FuncOutputProperty<Float2> Coordinate { get; }
     public FuncOutputProperty<Float2> Coordinate { get; }
 
 
     public FuncOutputProperty<Half4> Color { get; }
     public FuncOutputProperty<Half4> Color { get; }
-    
+
     public InputProperty<ColorSampleMode> SampleMode { get; }
     public InputProperty<ColorSampleMode> SampleMode { get; }
     public InputProperty<bool> NormalizeCoordinates { get; }
     public InputProperty<bool> NormalizeCoordinates { get; }
 
 
     public Guid OtherNode { get; set; }
     public Guid OtherNode { get; set; }
-    
+
     public ModifyImageLeftNode()
     public ModifyImageLeftNode()
     {
     {
         Image = CreateInput<Texture?>("Surface", "IMAGE", null);
         Image = CreateInput<Texture?>("Surface", "IMAGE", null);
@@ -33,42 +33,60 @@ public class ModifyImageLeftNode : Node, IPairNode, IPreviewRenderable
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
         SampleMode = CreateInput("SampleMode", "COLOR_SAMPLE_MODE", ColorSampleMode.ColorManaged);
         NormalizeCoordinates = CreateInput("NormalizeCoordinates", "NORMALIZE_COORDINATES", true);
         NormalizeCoordinates = CreateInput("NormalizeCoordinates", "NORMALIZE_COORDINATES", true);
     }
     }
-    
+
     private Half4 GetColor(FuncContext context)
     private Half4 GetColor(FuncContext context)
     {
     {
         context.ThrowOnMissingContext();
         context.ThrowOnMissingContext();
-        
-        if(Image.Value == null)
+
+        if (Image.Value == null)
         {
         {
             return new Half4("") { ConstantValue = Colors.Transparent };
             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)
     protected override void OnExecute(RenderContext context)
     {
     {
+        RenderPreviews(context);
     }
     }
 
 
     public override Node CreateCopy() => new ModifyImageLeftNode();
     public override Node CreateCopy() => new ModifyImageLeftNode();
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+
+    private void RenderPreviews(RenderContext context)
     {
     {
-        if(Image.Value == null)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
         {
-            return null;
-        } 
-        
-        return new RectD(0, 0, Image.Value.Size.X, Image.Value.Size.Y);
+            var texture = request.Texture;
+            if (texture is null) continue;
+
+            int saved = texture.DrawingSurface.Canvas.Save();
+
+            VecI size = Image.Value?.Size ?? context.RenderOutputSize;
+            VecD scaling = PreviewUtility.CalculateUniformScaling(size, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(size, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
+
+            texture.DrawingSurface.Canvas.Clear();
+            RenderPreview(texture.DrawingSurface);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
     }
     }
 
 
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public bool RenderPreview(DrawingSurface renderOn)
     {
     {
-        if(Image.Value is null)
+        if (Image.Value is null)
         {
         {
             return false;
             return false;
         }
         }
 
 
-        renderOn.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0); 
+        renderOn.Canvas.DrawSurface(Image.Value.DrawingSurface, 0, 0);
         return true;
         return true;
     }
     }
 }
 }

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

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Shaders.Generation.Expressions;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
@@ -37,7 +38,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
         RendersInAbsoluteCoordinates = true;
         RendersInAbsoluteCoordinates = true;
     }
     }
 
 
-    protected override void OnPaint(RenderContext renderContext, DrawingSurface targetSurface)
+    protected override void OnPaint(RenderContext renderContext, Canvas targetSurface)
     {
     {
         if (OtherNode == null || OtherNode == default)
         if (OtherNode == null || OtherNode == default)
         {
         {
@@ -95,7 +96,7 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             Half4 color = Color.NonOverridenValue(FuncContext.NoContext);
             color.VariableName = "color";
             color.VariableName = "color";
             builder.AddUniform(color.VariableName, color.ConstantValue);
             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();
         string sksl = builder.ToSkSl();
@@ -110,32 +111,25 @@ public class ModifyImageRightNode : RenderNode, IPairNode, ICustomShaderNode
             drawingPaint.Shader = drawingPaint.Shader.WithUpdatedUniforms(builder.Uniforms);
             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();
         builder.Dispose();
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
     {
     {
-        var startNode = FindStartNode();
-        if (startNode != null)
-        {
-            return startNode.GetPreviewBounds(frame, elementToRenderName);
-        }
-
-        return null;
+        return size.HasValue ? new RectD(0, 0, size.Value.X, size.Value.Y) : null;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
         var startNode = FindStartNode();
         var startNode = FindStartNode();
         if (drawingPaint != null && startNode is { Image.Value: not null })
         if (drawingPaint != null && startNode is { Image.Value: not null })
         {
         {
-            renderOn.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+            using var tmpTex = Texture.ForProcessing(startNode.Image.Value.Size, context.ProcessingColorSpace);
 
 
-            return true;
+            tmpTex.DrawingSurface.Canvas.DrawRect(0, 0, startNode.Image.Value.Size.X, startNode.Image.Value.Size.Y, drawingPaint);
+            renderOn.Canvas.DrawSurface(tmpTex.DrawingSurface, 0, 0);
         }
         }
-
-        return false;
     }
     }
 
 
     public override void Dispose()
     public override void 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;
+    }
+}

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

@@ -12,6 +12,7 @@ using Drawie.Backend.Core.Shaders;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
@@ -28,6 +29,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
     public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames;
     public event Action ConnectionsChanged;
     public event Action ConnectionsChanged;
+    public event Action OutputsChanged;
 
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
@@ -47,7 +49,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     private VecI lastRenderSize = new VecI(0, 0);
     private VecI lastRenderSize = new VecI(0, 0);
 
 
-    protected internal bool IsDisposed => _isDisposed;
+    public bool IsDisposed => _isDisposed;
     private bool _isDisposed;
     private bool _isDisposed;
 
 
     private int lastContentCacheHash = -1;
     private int lastContentCacheHash = -1;
@@ -83,13 +85,18 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     protected virtual bool CacheChanged(RenderContext context)
     protected virtual bool CacheChanged(RenderContext context)
     {
     {
-        bool changed = lastRenderSize != context.RenderOutputSize;
+        bool changed = false;
 
 
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Inputs))
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Inputs))
         {
         {
             changed |= inputs.Any(x => x.CacheChanged);
             changed |= inputs.Any(x => x.CacheChanged);
         }
         }
 
 
+        if (CacheTrigger.HasFlag(CacheTriggerFlags.RenderSize))
+        {
+            changed |= lastRenderSize != context.RenderOutputSize;
+        }
+
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Timeline))
         if (CacheTrigger.HasFlag(CacheTriggerFlags.Timeline))
         {
         {
             changed |= lastFrameTime.Frame != context.FrameTime.Frame ||
             changed |= lastFrameTime.Frame != context.FrameTime.Frame ||
@@ -117,7 +124,8 @@ public abstract class Node : IReadOnlyNode, IDisposable
         lastContentCacheHash = GetContentCacheHash();
         lastContentCacheHash = GetContentCacheHash();
     }
     }
 
 
-    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action, Func<IInputProperty, bool>? branchCondition = null)
+    public void TraverseBackwards(Func<IReadOnlyNode, IInputProperty, bool> action,
+        Func<IInputProperty, bool>? branchCondition = null)
     {
     {
         var visited = new HashSet<IReadOnlyNode>();
         var visited = new HashSet<IReadOnlyNode>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
         var queueNodes = new Queue<(IReadOnlyNode, IInputProperty)>();
@@ -143,6 +151,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
                 {
                 {
                     continue;
                     continue;
                 }
                 }
+
                 if (inputProperty.Connection != null)
                 if (inputProperty.Connection != null)
                 {
                 {
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
                     queueNodes.Enqueue((inputProperty.Connection.Node, inputProperty));
@@ -413,9 +422,17 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
         }
     }
     }
 
 
+
+    protected void RemoveOutputProperty(OutputProperty property)
+    {
+        outputs.Remove(property);
+        OutputsChanged?.Invoke();
+    }
+
     protected void AddOutputProperty(OutputProperty property)
     protected void AddOutputProperty(OutputProperty property)
     {
     {
         outputs.Add(property);
         outputs.Add(property);
+        OutputsChanged?.Invoke();
     }
     }
 
 
     protected void AddInputProperty(InputProperty property)
     protected void AddInputProperty(InputProperty property)
@@ -563,7 +580,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return GetOutputProperty(outputProperty);
         return GetOutputProperty(outputProperty);
     }
     }
 
 
-    public virtual void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    public virtual void SerializeAdditionalData(IReadOnlyDocument target, Dictionary<string, object> additionalData)
     {
     {
     }
     }
 
 
@@ -613,6 +630,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         hash.Add(GetType());
         hash.Add(GetType());
         hash.Add(DisplayName);
         hash.Add(DisplayName);
         hash.Add(Position);
         hash.Add(Position);
+
         foreach (var input in inputs)
         foreach (var input in inputs)
         {
         {
             hash.Add(input.GetCacheHash());
             hash.Add(input.GetCacheHash());

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

@@ -65,7 +65,7 @@ public class NoiseNode : RenderNode
         AngleOffset = CreateInput(nameof(AngleOffset), "ANGLE_OFFSET", 0d);
         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
         if (Math.Abs(previousScale - Scale.Value) > 0.000001
             || previousSeed != Seed.Value
             || previousSeed != Seed.Value
@@ -109,37 +109,31 @@ public class NoiseNode : RenderNode
         RenderNoise(target);
         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 RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return new RectD(0, 0, 128, 128); 
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
         var shader = SelectShader();
         var shader = SelectShader();
         if (shader == null)
         if (shader == null)
         {
         {
-            return false;
+            return;
         }
         }
 
 
         if (paint.Shader != voronoiShader)
         if (paint.Shader != voronoiShader)
         {
         {
             paint?.Shader?.Dispose();
             paint?.Shader?.Dispose();
         }
         }
+
         paint.Shader = shader;
         paint.Shader = shader;
         paint.ColorFilter = grayscaleFilter;
         paint.ColorFilter = grayscaleFilter;
         
         
-        RenderNoise(renderOn);
-
-        return true;
+        RenderNoise(renderOn.Canvas);
     }
     }
 
 
     private Shader SelectShader()
     private Shader SelectShader()

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

@@ -8,7 +8,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
 [NodeInfo("Output")]
 [NodeInfo("Output")]
-public class OutputNode : Node, IRenderInput, IPreviewRenderable
+public class OutputNode : Node, IRenderInput
 {
 {
     public const string UniqueName = "PixiEditor.Output";
     public const string UniqueName = "PixiEditor.Output";
     public const string InputPropertyName = "Background";
     public const string InputPropertyName = "Background";
@@ -34,33 +34,28 @@ public class OutputNode : Node, IRenderInput, IPreviewRenderable
 
 
         Input.Value?.Paint(context, context.RenderSurface);
         Input.Value?.Paint(context, context.RenderSurface);
         lastDocumentSize = context.DocumentSize;
         lastDocumentSize = context.DocumentSize;
-    }
-
-    RenderInputProperty IRenderInput.Background => Input;
-
-    public RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        if (lastDocumentSize == null)
-        {
-            return null;
-        }
 
 
-        return new RectD(0, 0, lastDocumentSize.Value.X, lastDocumentSize.Value.Y);
-    }
-
-    public bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        if (Input.Value == null)
+        var previews = context.GetPreviewTexturesForNode(Id);
+        if (previews is null) return;
+        foreach (var request in previews)
         {
         {
-            return false;
-        }
+            var texture = request.Texture;
+            if (texture is null) continue;
 
 
-        int saved = renderOn.Canvas.Save();
-        renderOn.Canvas.Scale((float)context.ChunkResolution.Multiplier());
-        Input.Value.Paint(context, renderOn);
+            int saved = texture.DrawingSurface.Canvas.Save();
 
 
-        renderOn.Canvas.RestoreToCount(saved);
+            VecD scaling = PreviewUtility.CalculateUniformScaling(context.DocumentSize, texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(context.DocumentSize, texture.Size, scaling);
+            texture.DrawingSurface.Canvas.Translate((float)offset.X, (float)offset.Y);
+            texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
+            var previewCtx =
+                PreviewUtility.CreatePreviewContext(context, scaling, context.RenderOutputSize, texture.Size);
 
 
-        return true;
+            texture.DrawingSurface.Canvas.Clear();
+            Input.Value?.Paint(previewCtx, texture.DrawingSurface.Canvas);
+            texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
     }
     }
+
+    RenderInputProperty IRenderInput.Background => Input;
 }
 }

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

+ 72 - 26
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -11,8 +11,9 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
-public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
+public abstract class RenderNode : Node, IHighDpiRenderNode
 {
 {
+    public const string OutputPropertyName = "Output";
     public RenderOutputProperty Output { get; }
     public RenderOutputProperty Output { get; }
 
 
     public bool AllowHighDpiRendering { get; set; } = false;
     public bool AllowHighDpiRendering { get; set; } = false;
@@ -26,7 +27,7 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
     public RenderNode()
     public RenderNode()
     {
     {
         Painter painter = new Painter(Paint);
         Painter painter = new Painter(Paint);
-        Output = CreateRenderOutput("Output", "OUTPUT",
+        Output = CreateRenderOutput(OutputPropertyName, "OUTPUT",
             () => painter,
             () => painter,
             () => this is IRenderInput renderInput ? renderInput.Background.Value : null);
             () => this is IRenderInput renderInput ? renderInput.Background.Value : null);
     }
     }
@@ -44,16 +45,18 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         lastDocumentSize = context.DocumentSize;
         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
         bool useIntermediate = !AllowHighDpiRendering
                                && context.RenderOutputSize is { X: > 0, Y: > 0 }
                                && context.RenderOutputSize is { X: > 0, Y: > 0 }
-                               && (surface.DeviceClipBounds.Size != context.RenderOutputSize || (RendersInAbsoluteCoordinates && !surface.Canvas.TotalMatrix.IsIdentity));
+                               && (surface.DeviceClipBounds.Size != context.RenderOutputSize ||
+                                   (RendersInAbsoluteCoordinates && !surface.TotalMatrix.IsIdentity));
         if (useIntermediate)
         if (useIntermediate)
         {
         {
-            Texture intermediate = textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
-            target = intermediate.DrawingSurface;
+            Texture intermediate =
+                textureCache.RequestTexture(-6451, context.RenderOutputSize, context.ProcessingColorSpace);
+            target = intermediate.DrawingSurface.Canvas;
         }
         }
 
 
         OnPaint(context, target);
         OnPaint(context, target);
@@ -62,41 +65,85 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         {
         {
             if (RendersInAbsoluteCoordinates)
             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)
             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
             else
             {
             {
-                surface.Canvas.DrawSurface(target, 0, 0);
+                surface.DrawSurface(target.Surface, 0, 0);
             }
             }
 
 
             if (RendersInAbsoluteCoordinates)
             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);
 
 
-    public virtual RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    protected void RenderPreviews(RenderContext ctx)
     {
     {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+        var previewToRender = ctx.GetPreviewTexturesForNode(Id);
+        if (previewToRender == null || previewToRender.Count == 0)
+            return;
+
+        foreach (var preview in previewToRender)
+        {
+            if (!ShouldRenderPreview(preview.ElementToRender))
+                continue;
+
+            if (preview.Texture == null)
+                continue;
+
+            int saved = preview.Texture.DrawingSurface.Canvas.Save();
+            preview.Texture.DrawingSurface.Canvas.Clear();
+
+            var bounds = GetPreviewBounds(ctx, preview.ElementToRender);
+            if (bounds == null)
+            {
+                bounds = new RectD(0, 0, ctx.RenderOutputSize.X, ctx.RenderOutputSize.Y);
+            }
+
+            VecD scaling = PreviewUtility.CalculateUniformScaling(bounds.Value.Size, preview.Texture.Size);
+            VecD offset = PreviewUtility.CalculateCenteringOffset(bounds.Value.Size, preview.Texture.Size, scaling);
+            RenderContext adjusted =
+                PreviewUtility.CreatePreviewContext(ctx, scaling, bounds.Value.Size, preview.Texture.Size);
+
+            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.Value.X, (float)-bounds.Value.Y);
+
+            adjusted.RenderSurface = preview.Texture.DrawingSurface.Canvas;
+            RenderPreview(preview.Texture.DrawingSurface, adjusted, preview.ElementToRender);
+            preview.Texture.DrawingSurface.Canvas.RestoreToCount(saved);
+        }
+    }
+
+    protected virtual bool ShouldRenderPreview(string elementToRenderName)
+    {
+        return true;
     }
     }
 
 
-    public virtual bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+    public virtual RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
+    {
+        return null;
+    }
+
+    public virtual void RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
         string elementToRenderName)
     {
     {
         int saved = renderOn.Canvas.Save();
         int saved = renderOn.Canvas.Save();
-        OnPaint(context, renderOn);
+        OnPaint(context, renderOn.Canvas);
         renderOn.Canvas.RestoreToCount(saved);
         renderOn.Canvas.RestoreToCount(saved);
-        return true;
     }
     }
 
 
     protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
@@ -104,25 +151,24 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         return textureCache.RequestTexture(id, size, processingCs, clear);
         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;
         additionalData["AllowHighDpiRendering"] = AllowHighDpiRendering;
     }
     }
 
 
-    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
+    internal override void DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data,
+        List<IChangeInfo> infos)
     {
     {
         base.DeserializeAdditionalData(target, data, infos);
         base.DeserializeAdditionalData(target, data, infos);
 
 
-        if(data.TryGetValue("AllowHighDpiRendering", out var value))
+        if (data.TryGetValue("AllowHighDpiRendering", out var value))
             AllowHighDpiRendering = (bool)value;
             AllowHighDpiRendering = (bool)value;
     }
     }
 
 
     public override void Dispose()
     public override void Dispose()
     {
     {
         base.Dispose();
         base.Dispose();
-        textureCache.Dispose(); 
+        textureCache.Dispose();
     }
     }
-
-   
 }
 }

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

@@ -24,7 +24,6 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     private string lastShaderCode;
     private string lastShaderCode;
     private Paint paint;
     private Paint paint;
 
 
-    private VecI lastDocumentSize;
     private List<Shader> lastCustomImageShaders = new();
     private List<Shader> lastCustomImageShaders = new();
 
 
     private Dictionary<string, (InputProperty prop, UniformValueType valueType)> uniformInputs = new();
     private Dictionary<string, (InputProperty prop, UniformValueType valueType)> uniformInputs = new();
@@ -75,7 +74,6 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
             shader = shader.WithUpdatedUniforms(uniforms);
             shader = shader.WithUpdatedUniforms(uniforms);
         }
         }
 
 
-        lastDocumentSize = context.DocumentSize;
         paint.Shader = shader;
         paint.Shader = shader;
     }
     }
 
 
@@ -125,11 +123,11 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         //texture.DrawingSurface.Canvas.Scale((float)context.ChunkResolution.Multiplier(), (float)context.ChunkResolution.Multiplier());
         //texture.DrawingSurface.Canvas.Scale((float)context.ChunkResolution.Multiplier(), (float)context.ChunkResolution.Multiplier());
 
 
         var ctx = context.Clone();
         var ctx = context.Clone();
-        ctx.RenderSurface = texture.DrawingSurface;
+        ctx.RenderSurface = texture.DrawingSurface.Canvas;
         ctx.RenderOutputSize = finalSize;
         ctx.RenderOutputSize = finalSize;
         ctx.ChunkResolution = ChunkResolution.Full;
         ctx.ChunkResolution = ChunkResolution.Full;
 
 
-        Background.Value.Paint(ctx, texture.DrawingSurface);
+        Background.Value.Paint(ctx, texture.DrawingSurface.Canvas);
         texture.DrawingSurface.Canvas.RestoreToCount(saved);
         texture.DrawingSurface.Canvas.RestoreToCount(saved);
 
 
         var snapshot = texture.DrawingSurface.Snapshot();
         var snapshot = texture.DrawingSurface.Snapshot();
@@ -144,15 +142,15 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         return uniforms;
         return uniforms;
     }
     }
 
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
     {
         if (shader == null || paint == null)
         if (shader == null || paint == null)
         {
         {
-            surface.Canvas.DrawColor(Colors.Magenta, BlendMode.Src);
+            surface.DrawColor(Colors.Magenta, BlendMode.Src);
             return;
             return;
         }
         }
 
 
-        DrawingSurface targetSurface = surface;
+        Canvas targetSurface = surface;
 
 
         float width = (float)(context.RenderOutputSize.X);
         float width = (float)(context.RenderOutputSize.X);
         float height = (float)(context.RenderOutputSize.Y);
         float height = (float)(context.RenderOutputSize.Y);
@@ -169,7 +167,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
                     : ColorSpace.Value == ColorSpaceType.Srgb
                     : ColorSpace.Value == ColorSpaceType.Srgb
                         ? Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()
                         ? Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgb()
                         : Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear());
                         : Drawie.Backend.Core.Surfaces.ImageData.ColorSpace.CreateSrgbLinear());
-            targetSurface = intermediateSurface.DrawingSurface;
+            targetSurface = intermediateSurface.DrawingSurface.Canvas;
             width = (float)(context.RenderOutputSize.X * context.ChunkResolution.InvertedMultiplier());
             width = (float)(context.RenderOutputSize.X * context.ChunkResolution.InvertedMultiplier());
             height = (float)(context.RenderOutputSize.Y * context.ChunkResolution.InvertedMultiplier());
             height = (float)(context.RenderOutputSize.Y * context.ChunkResolution.InvertedMultiplier());
             scale = true;
             scale = true;
@@ -181,41 +179,38 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
                 if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
                 if (ColorSpace.Value == ColorSpaceType.Srgb && !context.ProcessingColorSpace.IsSrgb)
                 {
                 {
                     targetSurface = RequestTexture(51, context.RenderOutputSize,
                     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)
                 else if (ColorSpace.Value == ColorSpaceType.LinearSrgb && context.ProcessingColorSpace.IsSrgb)
                 {
                 {
                     targetSurface = RequestTexture(51, context.RenderOutputSize,
                     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)
         if (targetSurface != surface)
         {
         {
-            int saved = surface.Canvas.Save();
+            int saved = surface.Save();
             if (scale)
             if (scale)
             {
             {
-                surface.Canvas.Scale((float)context.ChunkResolution.Multiplier(),
+                surface.Scale((float)context.ChunkResolution.Multiplier(),
                     (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);
         }
         }
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
-        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        OnPaint(context, renderOn);
-        return true;
+        int saved = renderOn.Canvas.Save();
+        renderOn.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
+        OnPaint(context, renderOn.Canvas);
+        renderOn.Canvas.RestoreToCount(saved);
     }
     }
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()

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

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

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

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 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();
+    }
+}

+ 10 - 7
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);
         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;
         var shape = Data.Value;
 
 
@@ -29,25 +29,28 @@ public class RasterizeShapeNode : RenderNode
 
 
         AllowHighDpiRendering = HighDpiRendering.Value;
         AllowHighDpiRendering = HighDpiRendering.Value;
 
 
-        shape.RasterizeTransformed(surface.Canvas);
+        shape.RasterizeTransformed(surface);
     }
     }
 
 
     public override Node CreateCopy() => new RasterizeShapeNode();
     public override Node CreateCopy() => new RasterizeShapeNode();
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName = "")
     {
     {
         return Data?.Value?.TransformedAABB;
         return Data?.Value?.TransformedAABB;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    protected override bool ShouldRenderPreview(string elementToRenderName)
+    {
+        return Data.Value != null && Data.Value.IsValid();
+    }
+
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
     {
     {
         var shape = Data.Value;
         var shape = Data.Value;
 
 
         if (shape == null || !shape.IsValid())
         if (shape == null || !shape.IsValid())
-            return false;
+            return;
 
 
         shape.RasterizeTransformed(renderOn.Canvas);
         shape.RasterizeTransformed(renderOn.Canvas);
-
-        return true;
     }
     }
 }
 }

+ 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);
         Output = CreateOutput<T>("Output", "OUTPUT", null);
     }
     }
     
     
-    private static readonly Paint rasterizePreviewPaint = new Paint();
 
 
     protected override void OnExecute(RenderContext context)
     protected override void OnExecute(RenderContext context)
     {
     {

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

@@ -47,8 +47,6 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
     public ChunkyImage? EmbeddedMask { get; set; }
     public ChunkyImage? EmbeddedMask { get; set; }
 
 
-    protected Texture renderedMask;
-
     protected static readonly Paint replacePaint =
     protected static readonly Paint replacePaint =
         new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
         new Paint() { BlendMode = Drawie.Backend.Core.Surfaces.BlendMode.Src };
 
 
@@ -131,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);
         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);
             RenderForOutput(context, renderTarget, Output);
         }
         }
     }
     }
 
 
-    private void OnFilterlessPaint(RenderContext context, DrawingSurface renderTarget)
+    private void OnFilterlessPaint(RenderContext context, Canvas renderTarget)
     {
     {
         RenderForOutput(context, renderTarget, FilterlessOutput);
         RenderForOutput(context, renderTarget, FilterlessOutput);
     }
     }
 
 
-    private void OnRawPaint(RenderContext context, DrawingSurface renderTarget)
+    private void OnRawPaint(RenderContext context, Canvas renderTarget)
     {
     {
         RenderForOutput(context, renderTarget, RawOutput);
         RenderForOutput(context, renderTarget, RawOutput);
     }
     }
@@ -157,7 +155,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetScenePosition(KeyFrameTime frameTime);
     public abstract VecD GetSceneSize(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)
         if (IsDisposed)
         {
         {
@@ -166,59 +164,82 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
         var renderObjectContext = CreateSceneContext(context, renderTarget, output);
 
 
-        int renderSaved = renderTarget.Canvas.Save();
+        int renderSaved = renderTarget.Save();
         VecD scenePos = GetScenePosition(context.FrameTime);
         VecD scenePos = GetScenePosition(context.FrameTime);
         VecD sceneSize = GetSceneSize(context.FrameTime);
         VecD sceneSize = GetSceneSize(context.FrameTime);
         //renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
         //renderTarget.Canvas.ClipRect(new RectD(scenePos - (sceneSize / 2f), sceneSize));
 
 
+        // Custom shader may modify the actual visible region, so we must force rendering full region
+        if (IsConnectedToCustomShaderNode(output))
+        {
+            renderObjectContext.VisibleDocumentRegion = null;
+        }
+
         Render(renderObjectContext);
         Render(renderObjectContext);
 
 
-        renderTarget?.Canvas.RestoreToCount(renderSaved);
+        renderTarget?.RestoreToCount(renderSaved);
+    }
+
+    private bool IsConnectedToCustomShaderNode(RenderOutputProperty output)
+    {
+        foreach (var conn in output.Connections)
+        {
+            bool isCustomShader = false;
+            conn.Node.TraverseForwards(x =>
+            {
+                if (x is ICustomShaderNode)
+                {
+                    isCustomShader = true;
+                    return false;
+                }
+
+                return true;
+            });
+
+            if (isCustomShader)
+                return true;
+        }
+
+        return false;
     }
     }
 
 
-    protected SceneObjectRenderContext CreateSceneContext(RenderContext context, DrawingSurface renderTarget,
+    protected SceneObjectRenderContext CreateSceneContext(RenderContext context, Canvas renderTarget,
         RenderOutputProperty output)
         RenderOutputProperty output)
     {
     {
         var sceneSize = GetSceneSize(context.FrameTime);
         var sceneSize = GetSceneSize(context.FrameTime);
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
         RectD localBounds = new RectD(0, 0, sceneSize.X, sceneSize.Y);
 
 
         SceneObjectRenderContext renderObjectContext = new SceneObjectRenderContext(output, renderTarget, localBounds,
         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.FrameTime, context.ChunkResolution, context.RenderOutputSize, context.DocumentSize,
+            renderTarget == context.RenderSurface,
+            context.ProcessingColorSpace, context.DesiredSamplingOptions, context.Graph, context.Opacity);
         renderObjectContext.FullRerender = context.FullRerender;
         renderObjectContext.FullRerender = context.FullRerender;
+        renderObjectContext.AffectedArea = context.AffectedArea;
+        renderObjectContext.VisibleDocumentRegion = context.VisibleDocumentRegion;
+        renderObjectContext.PreviewTextures = context.PreviewTextures;
         return renderObjectContext;
         return renderObjectContext;
     }
     }
 
 
     public abstract void Render(SceneObjectRenderContext sceneContext);
     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 (MaskIsVisible.Value)
         {
         {
             if (CustomMask.Value != null)
             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);
                 CustomMask.Value.Paint(context, surface);
 
 
-                surface.Canvas.RestoreToCount(layer);
+                surface.RestoreToCount(layer);
             }
             }
-            else if (EmbeddedMask != null)
+            else
             {
             {
-                if (context.FullRerender)
-                {
-                    EmbeddedMask.DrawMostUpToDateRegionOn(
-                        new RectI(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y),
-                        ChunkResolution.Full,
-                        surface, VecI.Zero, maskPaint);
-                }
-                else if (renderedMask != null)
-                {
-                    int saved = surface.Canvas.Save();
-                    surface.Canvas.Scale((float)renderResolution.Multiplier());
-                    surface.Canvas.DrawSurface(renderedMask.DrawingSurface, 0, 0, maskPaint);
-                    surface.Canvas.RestoreToCount(saved);
-                }
+                EmbeddedMask?.DrawMostUpToDateRegionOn(
+                    new RectI(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y),
+                    context.ChunkResolution,
+                    surface, VecI.Zero, maskPaint, drawPaintOnEmpty: true);
             }
             }
         }
         }
     }
     }
@@ -229,48 +250,6 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
             ClipToPreviousMember ? 1 : 0);
             ClipToPreviousMember ? 1 : 0);
     }
     }
 
 
-    public virtual void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
-        ColorSpace processingColorSpace)
-    {
-        RenderChunkyImageChunk(chunkPos, resolution, EmbeddedMask, 55, processingColorSpace, ref renderedMask);
-    }
-
-    protected void RenderChunkyImageChunk(VecI chunkPos, ChunkResolution resolution, ChunkyImage img,
-        int textureId, ColorSpace processingColorSpace,
-        ref Texture? renderSurface)
-    {
-        if (img is null)
-        {
-            return;
-        }
-
-        VecI targetSize = img.LatestSize;
-
-        if (targetSize.X <= 0 || targetSize.Y <= 0)
-        {
-            return;
-        }
-
-        using var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
-        renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
-
-        int saved = renderSurface.DrawingSurface.Canvas.Save();
-
-        if (!img.DrawMostUpToDateChunkOn(
-                chunkPos,
-                ChunkResolution.Full,
-                renderSurface.DrawingSurface,
-                chunkPos * ChunkResolution.Full.PixelSize(),
-                replacePaint))
-        {
-            var chunkSize = ChunkResolution.Full.PixelSize();
-            renderSurface.DrawingSurface.Canvas.DrawRect(new RectD(chunkPos * chunkSize, new VecD(chunkSize)),
-                clearPaint);
-        }
-
-        renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
-    }
-
     protected void ApplyRasterClip(DrawingSurface toClip, DrawingSurface clipSource)
     protected void ApplyRasterClip(DrawingSurface toClip, DrawingSurface clipSource)
     {
     {
         if (ClipToPreviousMember && Background.Value != null)
         if (ClipToPreviousMember && Background.Value != null)
@@ -289,7 +268,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         return (MaskIsVisible.Value && (EmbeddedMask != null || CustomMask.Value != null)) || ClipToPreviousMember;
         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;
         blendPaint.Color = Colors.White;
         clipSource.DrawClipSource(context, drawOnto);
         clipSource.DrawClipSource(context, drawOnto);
@@ -298,9 +277,9 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
     public abstract RectD? GetTightBounds(KeyFrameTime frameTime);
     public abstract RectD? GetApproxBounds(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)
         if (EmbeddedMask != null)
         {
         {
             additionalData["embeddedMask"] = EmbeddedMask;
             additionalData["embeddedMask"] = EmbeddedMask;
@@ -335,40 +314,45 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
         }
     }
     }
 
 
-    public override RectD? GetPreviewBounds(int frame, string elementFor = "")
+    protected override bool ShouldRenderPreview(string elementToRenderName)
     {
     {
-        if (elementFor == nameof(EmbeddedMask) && EmbeddedMask != null)
+        if (elementToRenderName == nameof(EmbeddedMask))
         {
         {
-            return new RectD(VecD.Zero, EmbeddedMask.LatestSize);
+            return true;
         }
         }
 
 
-        return null;
+        return EmbeddedMask != null;
     }
     }
 
 
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
-        string elementToRenderName)
+    public override RectD? GetPreviewBounds(RenderContext ctx, string elementToRenderName)
     {
     {
-        if (elementToRenderName != nameof(EmbeddedMask))
-        {
-            return false;
-        }
+        return EmbeddedMask is null
+            ? null
+            : new RectD(0, 0, EmbeddedMask.LatestSize.X, EmbeddedMask.LatestSize.Y);
+    }
 
 
+    public override void RenderPreview(DrawingSurface renderOn, RenderContext context,
+        string elementToRenderName)
+    {
         var img = EmbeddedMask;
         var img = EmbeddedMask;
 
 
         if (img is null)
         if (img is null)
         {
         {
-            return false;
+            return;
         }
         }
 
 
-        renderOn.Canvas.DrawSurface(renderedMask.DrawingSurface, VecI.Zero, maskPreviewPaint);
-
-        return true;
+        int saved = renderOn.Canvas.Save();
+        renderOn.Canvas.Scale((float)context.ChunkResolution.InvertedMultiplier());
+        img.DrawMostUpToDateRegionOn(
+            new RectI(0, 0, img.LatestSize.X, img.LatestSize.Y),
+            context.ChunkResolution,
+            renderOn.Canvas, VecI.Zero, maskPreviewPaint, drawPaintOnEmpty: true);
+        renderOn.Canvas.RestoreToCount(saved);
     }
     }
 
 
     public override void Dispose()
     public override void Dispose()
     {
     {
         base.Dispose();
         base.Dispose();
-        renderedMask?.Dispose();
         EmbeddedMask?.Dispose();
         EmbeddedMask?.Dispose();
         Output.Value = null;
         Output.Value = null;
         maskPaint.Dispose();
         maskPaint.Dispose();

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

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

@@ -57,27 +57,16 @@ public class TileNode : RenderNode
         paint.Shader = tileShader;
         paint.Shader = tileShader;
     }
     }
 
 
-    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    protected override void OnPaint(RenderContext context, Canvas surface)
     {
     {
         if (paint == null)
         if (paint == null)
             return;
             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()
     public override Node CreateCopy()
     {
     {
         return new TileNode();
         return new TileNode();
     }
     }
-
-    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
-    {
-        return null;
-    }
-
-    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
-    {
-        return false;
-    }
-
 }
 }

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