Browse Source

fixed disposing document

flabbet 1 year ago
parent
commit
6f68d0f413

+ 20 - 5
src/ChunkyImageLib/Chunk.cs

@@ -21,7 +21,18 @@ public class Chunk : IDisposable
     /// <summary>
     /// The surface of the chunk
     /// </summary>
-    public Surface Surface { get; }
+    public Surface Surface
+    {
+        get
+        {
+            if (returned)
+            {
+                throw new ObjectDisposedException("Chunk has been disposed");
+            }
+            
+            return internalSurface;
+        }
+    }
 
     /// <summary>
     /// The size of the chunk
@@ -32,13 +43,17 @@ public class Chunk : IDisposable
     /// The resolution of the chunk
     /// </summary>
     public ChunkResolution Resolution { get; }
+    
+    public bool Disposed => returned;
+
+    private Surface internalSurface;
     private Chunk(ChunkResolution resolution)
     {
         int size = resolution.PixelSize();
 
         Resolution = resolution;
         PixelSize = new(size, size);
-        Surface = new Surface(PixelSize);
+        internalSurface = new Surface(PixelSize);
     }
 
     /// <summary>
@@ -57,7 +72,7 @@ public class Chunk : IDisposable
     /// </summary>
     /// <param name="pos">The destination for the <paramref name="surface"/></param>
     /// <param name="paint">The paint to use while drawing</param>
-    public void DrawOnSurface(DrawingSurface surface, VecI pos, Paint? paint = null)
+    public void DrawChunkOn(DrawingSurface surface, VecI pos, Paint? paint = null)
     {
         surface.Canvas.DrawSurface(Surface.DrawingSurface, pos.X, pos.Y, paint);
     }
@@ -99,9 +114,9 @@ public class Chunk : IDisposable
     {
         if (returned)
             return;
-        returned = true;
         Interlocked.Decrement(ref chunkCounter);
-        Surface.DrawingSurface.Canvas.RestoreToCount(-1);
+        Surface.DrawingSurface.Canvas.Clear();
         ChunkPool.Instance.Push(this);
+        returned = true;
     }
 }

+ 112 - 53
src/ChunkyImageLib/ChunkyImage.cs

@@ -62,10 +62,13 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
     private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
     private static Paint InverseClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstOut };
     private static Paint ReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src };
-    private static Paint SmoothReplacingPaint { get; } = new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
+
+    private static Paint SmoothReplacingPaint { get; } =
+        new Paint() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium };
+
     private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
     private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
-    
+
     public int CommitCounter => commitCounter;
 
     public VecI CommittedSize { get; private set; }
@@ -88,7 +91,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
     private double? horizontalSymmetryAxis = null;
     private double? verticalSymmetryAxis = null;
     private float opacity = 1;
-    
+
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
@@ -133,6 +136,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                 rect ??= chunkBounds;
                 rect = rect.Value.Union(chunkBounds);
             }
+
             foreach (var operation in queuedOperations)
             {
                 foreach (var pos in operation.affectedArea.Chunks)
@@ -142,6 +146,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                     rect = rect.Value.Union(chunkBounds);
                 }
             }
+
             return rect;
         }
     }
@@ -159,6 +164,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                 rect ??= chunkBounds;
                 rect = rect.Value.Union(chunkBounds);
             }
+
             return rect;
         }
     }
@@ -182,7 +188,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             {
                 if (committedChunks[suggestedResolution].TryGetValue(chunkPos, out Chunk? requestedResChunk))
                 {
-                    RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize)).Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
+                    RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
+                        .Intersect(scaledCommittedSize).Translate(-chunkPos * chunkSize);
 
                     RectI? chunkPreciseBounds = requestedResChunk.FindPreciseBounds(visibleArea);
                     if (chunkPreciseBounds is null)
@@ -194,17 +201,20 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                 }
                 else
                 {
-                    RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize)).Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
+                    RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
+                        .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
 
                     RectI? chunkPreciseBounds = fullResChunk.FindPreciseBounds(visibleArea);
                     if (chunkPreciseBounds is null)
                         continue;
-                    RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier).Offset(chunkPos * chunkSize).RoundOutwards();
+                    RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
+                        .Offset(chunkPos * chunkSize).RoundOutwards();
 
                     preciseBounds ??= globalChunkBounds;
                     preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
                 }
             }
+
             preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
             preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
 
@@ -284,12 +294,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             {
                 Chunk? committedChunk = MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full);
                 Chunk? latestChunk = GetLatestChunk(chunkPos, ChunkResolution.Full);
-                Color committedColor = committedChunk is null ?
-                    Colors.Transparent :
-                    committedChunk.Surface.GetSRGBPixel(posInChunk);
-                Color latestColor = latestChunk is null ?
-                    Colors.Transparent :
-                    latestChunk.Surface.GetSRGBPixel(posInChunk);
+                Color committedColor = committedChunk is null
+                    ? Colors.Transparent
+                    : committedChunk.Surface.GetSRGBPixel(posInChunk);
+                Color latestColor = latestChunk is null
+                    ? Colors.Transparent
+                    : latestChunk.Surface.GetSRGBPixel(posInChunk);
                 // using a whole chunk just to draw 1 pixel is kinda dumb,
                 // but this should be faster than any approach that requires allocations
                 using Chunk tempChunk = Chunk.Create(ChunkResolution.Eighth);
@@ -306,7 +316,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
     /// True if the chunk existed and was drawn, otherwise false
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
+    public bool DrawMostUpToDateChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
+        Paint? paint = null)
     {
         lock (lockObject)
         {
@@ -331,7 +342,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             {
                 if (committedChunk is null)
                     return false;
-                committedChunk.DrawOnSurface(surface, pos, paint);
+                committedChunk.DrawChunkOn(surface, pos, paint);
                 return true;
             }
 
@@ -340,7 +351,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             {
                 if (latestChunk.IsT2)
                 {
-                    latestChunk.AsT2.DrawOnSurface(surface, pos, paint);
+                    latestChunk.AsT2.DrawChunkOn(surface, pos, paint);
                     return true;
                 }
 
@@ -349,12 +360,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
 
             // combine with committed and then draw
             using var tempChunk = Chunk.Create(resolution);
-            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0, ReplacingPaint);
+            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);
+            tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.AsT2.Surface.DrawingSurface, 0, 0,
+                blendModePaint);
             if (lockTransparency)
                 OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface, committedChunk.Surface.DrawingSurface);
-            tempChunk.DrawOnSurface(surface, pos, paint);
+            tempChunk.DrawChunkOn(surface, pos, paint);
 
             return true;
         }
@@ -391,12 +404,13 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                     return true;
             }
         }
-        
+
         return false;
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos, Paint? paint = null)
+    public bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecI pos,
+        Paint? paint = null)
     {
         lock (lockObject)
         {
@@ -404,7 +418,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             var chunk = GetCommittedChunk(chunkPos, resolution);
             if (chunk is null)
                 return false;
-            chunk.DrawOnSurface(surface, pos, paint);
+            chunk.DrawChunkOn(surface, pos, paint);
             return true;
         }
     }
@@ -461,7 +475,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be executed when there are no queued operations");
             activeClips.Add(clippingMask);
         }
     }
@@ -473,7 +488,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be executed when there are no queued operations");
             this.clippingPath = clippingPath;
         }
     }
@@ -488,7 +504,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be executed when there are no queued operations");
             blendMode = mode;
         }
     }
@@ -500,7 +517,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be executed when there are no queued operations");
             horizontalSymmetryAxis = position;
         }
     }
@@ -512,7 +530,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be executed when there are no queued operations");
             verticalSymmetryAxis = position;
         }
     }
@@ -550,7 +569,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth, Paint? paint = null)
+    public void EnqueueDrawEllipse(RectI location, Color strokeColor, Color fillColor, int strokeWidth,
+        Paint? paint = null)
     {
         lock (lockObject)
         {
@@ -604,7 +624,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             EnqueueOperation(operation);
         }
     }
-    
+
     public void EnqueueApplyMask(ChunkyImage mask)
     {
         lock (lockObject)
@@ -617,7 +637,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
 
     /// <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, Color color, float strokeWidth, StrokeCap strokeCap, BlendMode blendMode, RectI? customBounds = null)
+    public void EnqueueDrawPath(VectorPath path, Color color, float strokeWidth, StrokeCap strokeCap,
+        BlendMode blendMode, RectI? customBounds = null)
     {
         lock (lockObject)
         {
@@ -639,7 +660,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void EnqueueDrawSkiaLine(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color, BlendMode blendMode)
+    public void EnqueueDrawSkiaLine(VecI from, VecI to, StrokeCap strokeCap, float strokeWidth, Color color,
+        BlendMode blendMode)
     {
         lock (lockObject)
         {
@@ -692,13 +714,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             EnqueueOperation(operation);
         }
     }
+
     public void EnqueueDrawUpToDateChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {
         ThrowIfDisposed();
         ChunkyImageOperation operation = new(image, pos, flipHor, flipVer, true);
         EnqueueOperation(operation);
     }
-    
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueClearRegion(RectI region)
     {
@@ -743,6 +766,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
         }
     }
+    
+    
+    public void EnqueueDrawPaint(Paint paint)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PaintOperation operation = new(paint);
+            EnqueueOperation(operation);
+        }
+    }
 
     private void EnqueueOperation(IDrawOperation operation)
     {
@@ -861,7 +895,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                 {
                     if (resolution == ChunkResolution.Full)
                     {
-                        throw new InvalidOperationException("Trying to commit a full res chunk that wasn't fully processed");
+                        throw new InvalidOperationException(
+                            "Trying to commit a full res chunk that wasn't fully processed");
                     }
                     else
                     {
@@ -910,13 +945,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
                     if (lockTransparency)
                     {
                         using Chunk tempChunk = Chunk.Create(resolution);
-                        tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0, ReplacingPaint);
-                        maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0, blendModePaint);
-                        OperationHelper.ClampAlpha(maybeCommitted.Surface.DrawingSurface, tempChunk.Surface.DrawingSurface);
+                        tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(maybeCommitted.Surface.DrawingSurface, 0, 0,
+                            ReplacingPaint);
+                        maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
+                            blendModePaint);
+                        OperationHelper.ClampAlpha(maybeCommitted.Surface.DrawingSurface,
+                            tempChunk.Surface.DrawingSurface);
                     }
                     else
                     {
-                        maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0, blendModePaint);
+                        maybeCommitted.Surface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, 0, 0,
+                            blendModePaint);
                     }
 
                     chunk.Dispose();
@@ -931,7 +970,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             {
                 if (resolution == ChunkResolution.Full)
                     continue;
-                if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) || halfChunk.QueueProgress != queuedOperations.Count)
+                if (!latestChunksData[resolution].TryGetValue(pos, out var halfChunk) ||
+                    halfChunk.QueueProgress != queuedOperations.Count)
                 {
                     if (committedChunks[resolution].TryGetValue(pos, out var committedLowResChunk))
                     {
@@ -990,7 +1030,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             ThrowIfDisposed();
             var chunks = new HashSet<VecI>();
             RectI? rect = null;
-            
+
             for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
             {
                 var (_, area) = queuedOperations[i];
@@ -1005,13 +1045,25 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         }
     }
 
+    public void SetCommitedChunk(Chunk chunk, VecI pos, ChunkResolution resolution)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            committedChunks[resolution][pos] = chunk;
+        }
+    }
+
     /// <summary>
     /// Applies all operations queued for a specific (latest) chunk. If the latest chunk doesn't exist yet, creates it. If none of the existing operations affect the chunk does nothing.
     /// </summary>
     private void MaybeCreateAndProcessQueueForChunk(VecI chunkPos, ChunkResolution resolution)
     {
         if (!latestChunksData[resolution].TryGetValue(chunkPos, out LatestChunkData chunkData))
-            chunkData = new() { QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos) };
+            chunkData = new()
+            {
+                QueueProgress = 0, IsDeleted = !committedChunks[ChunkResolution.Full].ContainsKey(chunkPos)
+            };
         if (chunkData.QueueProgress == queuedOperations.Count)
             return;
 
@@ -1034,12 +1086,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             }
 
             if (chunkData.QueueProgress <= i)
-                chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
+                chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!,
+                    chunkPos, resolution, chunkData);
         }
 
         if (initialized)
         {
-            if (lockTransparency && !chunkData.IsDeleted && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
+            if (lockTransparency && !chunkData.IsDeleted &&
+                MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
             {
                 var committed = GetCommittedChunk(chunkPos, resolution);
                 OperationHelper.ClampAlpha(targetChunk!.Surface.DrawingSurface, committed!.Surface.DrawingSurface);
@@ -1072,7 +1126,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             if (mask.CommittedChunkExists(chunkPos))
             {
-                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
+                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.DrawingSurface, VecI.Zero,
+                    ClippingPaint);
             }
             else
             {
@@ -1118,14 +1173,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             var clip = combinedRasterClips.AsT2;
 
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
-            targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
+            targetChunk.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
 
             CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
 
-            clip.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
-            clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
+            clip.DrawChunkOn(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
+            clip.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
 
-            tempChunk.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
+            tempChunk.DrawChunkOn(targetChunk.Surface.DrawingSurface, VecI.Zero, AddingPaint);
             return false;
         }
 
@@ -1137,7 +1192,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         return chunkData.IsDeleted;
     }
 
-    private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
+    private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk,
+        ChunkResolution resolution, VecI chunkPos)
     {
         if (operationAffectedArea is null)
             return;
@@ -1149,7 +1205,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             using VectorPath transformedPath = new(clippingPath);
             VecD trans = -chunkPos * FullChunkSize * scale;
-            
+
             transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
             targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
         }
@@ -1159,7 +1215,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         affectedAreaPos = (affectedAreaPos - chunkPos * FullChunkSize) * scale;
         affectedAreaSize = affectedAreaSize * scale;
         targetChunk.Surface.DrawingSurface.Canvas.ClipRect(new RectD(affectedAreaPos, affectedAreaSize));
-        
+
         operation.DrawOnChunk(targetChunk, chunkPos);
         targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
     }
@@ -1175,7 +1231,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
         {
             ThrowIfDisposed();
             if (queuedOperations.Count > 0)
-                throw new InvalidOperationException("This function can only be used when there are no queued operations");
+                throw new InvalidOperationException(
+                    "This function can only be used when there are no queued operations");
             FindAndDeleteEmptyCommittedChunks();
             return committedChunks[ChunkResolution.Full].Count == 0;
         }
@@ -1190,7 +1247,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
 
     private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
     {
-        return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X || chunkPos.Y * FullChunkSize >= imageSize.Y;
+        return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X ||
+               chunkPos.Y * FullChunkSize >= imageSize.Y;
     }
 
     private void FindAndDeleteEmptyCommittedChunks()
@@ -1242,7 +1300,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             newChunk.Surface.DrawingSurface.Canvas.Save();
             newChunk.Surface.DrawingSurface.Canvas.Scale((float)resolution.Multiplier());
 
-            newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0, SmoothReplacingPaint);
+            newChunk.Surface.DrawingSurface.Canvas.DrawSurface(existingFullResChunk.Surface.DrawingSurface, 0, 0,
+                SmoothReplacingPaint);
             newChunk.Surface.DrawingSurface.Canvas.Restore();
             committedChunks[resolution][chunkPos] = newChunk;
             return newChunk;
@@ -1348,5 +1407,5 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             ChunkyImage clone = CloneFromCommitted();
             return clone;
         }
-    } 
+    }
 }

+ 33 - 0
src/ChunkyImageLib/Operations/PaintOperation.cs

@@ -0,0 +1,33 @@
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace ChunkyImageLib.Operations;
+
+public class PaintOperation : IDrawOperation
+{
+    private Paint paint;
+
+    public PaintOperation(Paint paint)
+    {
+        this.paint = paint;
+    }
+    
+    public void Dispose()
+    {
+        
+    }
+
+    public bool IgnoreEmptyChunks => false;
+    public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)
+    {
+        targetChunk.Surface.DrawingSurface.Canvas.DrawPaint(paint);
+    }
+
+    public AffectedArea FindAffectedArea(VecI imageSize)
+    {
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            new RectI(0, 0, imageSize.X, imageSize.Y), 
+            ChunkyImage.FullChunkSize));
+    }
+}

+ 8 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs

@@ -163,6 +163,14 @@ internal class ActionAccumulator
                         document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
                     }
                     break;
+                case NodePreviewDirty_RenderInfo info:
+                    {
+                        var node = document.StructureHelper.Find(info.NodeId);
+                        if (node is null || node.PreviewSurface is null)
+                            continue;
+                        node.PreviewSurface.AddDirtyRect(new RectI(0, 0, node.PreviewSurface.Size.X, node.PreviewSurface.Size.Y));
+                    }
+                    break;
             }
         }
     }

+ 40 - 35
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -73,7 +73,7 @@ internal class MemberPreviewUpdater
         }).ConfigureAwait(true);
 
         RecreatePreviewBitmaps(changedMainPreviewBounds!, changedMaskPreviewBounds!);
-        
+
         var renderInfos = await Task.Run(() => Render(changedMainPreviewBounds!, changedMaskPreviewBounds))
             .ConfigureAwait(true);
 
@@ -388,7 +388,7 @@ internal class MemberPreviewUpdater
         RenderWholeCanvasPreview(mainPreviewChunksToRerender, maskPreviewChunksToRerender, infos);
         RenderMainPreviews(mainPreviewChunksToRerender, recreatedMainPreviewSizes, infos);
         RenderMaskPreviews(maskPreviewChunksToRerender, recreatedMaskPreviewSizes, infos);
-        RenderNodePreviews();
+        RenderNodePreviews(infos);
 
         return infos;
 
@@ -451,7 +451,7 @@ internal class MemberPreviewUpdater
             else if (rendered.IsT0)
             {
                 using var renderedChunk = rendered.AsT0;
-                renderedChunk.DrawOnSurface(doc.PreviewSurface.DrawingSurface, pos, SmoothReplacingPaint);
+                renderedChunk.DrawChunkOn(doc.PreviewSurface.DrawingSurface, pos, SmoothReplacingPaint);
             }
 
             doc.PreviewSurface.DrawingSurface.Canvas.Restore();
@@ -513,7 +513,7 @@ internal class MemberPreviewUpdater
                     {
                         foreach (var child in group.Children)
                         {
-                            if (member is IReadOnlyImageNode rasterLayer) 
+                            if (member is IReadOnlyImageNode rasterLayer)
                             {
                                 RenderAnimationFramePreview(rasterLayer, child, affArea.Value);
                             }
@@ -538,7 +538,8 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> folder
     /// </summary>
-    private void RenderFolderMainPreview(IReadOnlyFolderNode folder, IStructureMemberHandler memberVM, AffectedArea area,
+    private void RenderFolderMainPreview(IReadOnlyFolderNode folder, IStructureMemberHandler memberVM,
+        AffectedArea area,
         VecI position, float scaling)
     {
         memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
@@ -584,7 +585,7 @@ internal class MemberPreviewUpdater
             var pos = chunk * ChunkResolution.Full.PixelSize();
             if (layer is not IReadOnlyImageNode raster) return;
             IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(doc.AnimationHandler.ActiveFrameBindable);
-            
+
             if (!result.DrawCommittedChunkOn(
                     chunk,
                     ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos,
@@ -602,9 +603,10 @@ internal class MemberPreviewUpdater
     {
         if (keyFrameVM.PreviewSurface is null)
         {
-            keyFrameVM.PreviewSurface = new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
+            keyFrameVM.PreviewSurface =
+                new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
         }
-        
+
         keyFrameVM.PreviewSurface!.DrawingSurface.Canvas.Save();
         float scaling = (float)keyFrameVM.PreviewSurface.Size.X / internals.Tracker.Document.Size.X;
         keyFrameVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
@@ -681,37 +683,40 @@ internal class MemberPreviewUpdater
             infos.Add(new MaskPreviewDirty_RenderInfo(guid));
         }
     }
-    
-    private void RenderNodePreviews()
+
+    private void RenderNodePreviews(List<IRenderInfo> infos)
     {
-        // TODO: recreate only changed previews
-        internals.Tracker.Document.NodeGraph.TryTraverse(node =>
+        internals.Tracker.Document.NodeGraph.TryTraverse((node) =>
         {
-            if (node.CachedResult is { IsDisposed: false })
+            if (node is null)
+                return;
+
+            if (node.CachedResult == null)
+            {
+                return;
+            }
+
+            var nodeVm = doc.StructureHelper.FindNode<INodeHandler>(node.Id);
+            if (nodeVm.ResultPreview == null)
             {
-               var nodeVm = doc.StructureHelper.FindNode<INodeHandler>(node.Id);
-
-               // TODO: do it in recreate preview bitmaps
-               if (nodeVm.ResultPreview == null)
-               {
-                     nodeVm.ResultPreview = new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
-               }
-               
-               float scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.Size.X;
-               float scalingY = (float)nodeVm.ResultPreview.Size.Y / node.CachedResult.Size.Y;
-               
-               nodeVm.ResultPreview.DrawingSurface.Canvas.Save();
-               nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
-               
-               nodeVm.ResultPreview.DrawingSurface.Canvas.Clear();
-
-               if (node.CachedResult != null)
-               {
-                   nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0, ReplacingPaint);
-               }
-
-               nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
+                nodeVm.ResultPreview =
+                    new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
             }
+
+            float scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.LatestSize.X;
+            float scalingY = (float)nodeVm.ResultPreview.Size.Y / node.CachedResult.LatestSize.Y;
+
+            nodeVm.ResultPreview.DrawingSurface.Canvas.Save();
+            nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
+
+            RectI region = new RectI(0, 0, node.CachedResult.LatestSize.X, node.CachedResult.LatestSize.Y);
+            
+            node.CachedResult.DrawMostUpToDateRegionOn(region, ChunkResolution.Full, nodeVm.ResultPreview.DrawingSurface,
+                VecI.Zero,
+                scalingX < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
+
+            nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
+            infos.Add(new NodePreviewDirty_RenderInfo(node.Id));
         });
     }
 }

+ 3 - 0
src/PixiEditor.AvaloniaUI/Models/Rendering/RenderInfos/NodePreviewDirty_RenderInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.AvaloniaUI.Models.Rendering.RenderInfos;
+
+public record NodePreviewDirty_RenderInfo(Guid NodeId) : IRenderInfo;

+ 12 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -845,4 +845,16 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             File.Delete(file);
         }
     }
+
+    public void Dispose()
+    {
+        foreach (var (_, surface) in Surfaces)
+        {
+            surface.Dispose();
+        }
+
+        PreviewSurface.Dispose();
+        Internals.Tracker.Dispose();
+        Internals.Tracker.Document.Dispose();
+    }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/ViewModelMain.cs

@@ -282,7 +282,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
             // So they remain alive and keep "showing" the now disposed DocumentViewModel
             // And since they reference the DocumentViewModel it doesn't get collected by GC
 
-            // document.Dispose();
+            document.Dispose();
             WindowSubViewModel.CloseViewportsForDocument(document);
 
             return true;

+ 5 - 1
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -100,7 +100,9 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         }
         else
         {
-            image = new Surface(layer.Execute(new RenderingContext(frame, Size)));
+            return null;
+            /*TODO: this*/
+            // image = new Surface(layer.Execute(new RenderingContext(frame, Size)));
         }
         
         //todo: idk if it's correct
@@ -241,6 +243,8 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     {
         return NodeGraph.Nodes.FirstOrDefault(x => x.Id == guid);
     }
+    
+    IReadOnlyNode IReadOnlyDocument.FindNode(Guid guid) => FindNodeOrThrow<Node>(guid);
 
     public T? FindNode<T>(Guid guid) where T : Node
     {

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -8,7 +8,7 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
-public interface IReadOnlyDocument
+public interface IReadOnlyDocument : IDisposable
 {    
     /// <summary>
     /// The root folder of the document
@@ -55,6 +55,8 @@ public interface IReadOnlyDocument
     public Image? GetLayerRasterizedImage(Guid layerGuid, int frame);
     public RectI? GetChunkAlignedLayerBounds(Guid layerGuid, int frame);
 
+    IReadOnlyNode FindNode(Guid guid);
+    
     /// <summary>
     /// Finds the member with the <paramref name="guid"/> or returns null if not found
     /// </summary>

+ 7 - 4
src/PixiEditor.ChangeableDocument/Rendering/RenderingContext.cs

@@ -18,17 +18,19 @@ public class RenderingContext : IDisposable
     public KeyFrameTime FrameTime { get; }
     public VecI ChunkToUpdate { get; }
     public ChunkResolution ChunkResolution { get; }
-    
+    public VecI DocumentSize { get; set; }
+
     /// <summary>
     ///     This surface is unique to each rendering context and is used to draw on to avoid leaking
     /// internal node surfaces and cloning them. It is disposed after rendering.
     /// </summary>
-    public Surface WorkingSurface { get; }
+    //public Surface WorkingSurface { get; }
 
     public RenderingContext(KeyFrameTime frameTime, VecI docSize)
     {
         FrameTime = frameTime;
-        WorkingSurface = new Surface(docSize);
+        DocumentSize = docSize;
+        //WorkingSurface = new Surface(docSize);
     }
     
     public RenderingContext(KeyFrameTime frameTime, VecI chunkToUpdate, ChunkResolution chunkResolution, VecI docSize)
@@ -36,7 +38,8 @@ public class RenderingContext : IDisposable
         FrameTime = frameTime;
         ChunkToUpdate = chunkToUpdate;
         ChunkResolution = chunkResolution;
-        WorkingSurface = new Surface(docSize);
+        DocumentSize = docSize;
+        //WorkingSurface = new Surface(docSize);
     }
 
     public static DrawingApiBlendMode GetDrawingBlendMode(BlendMode blendMode)