Browse Source

Improve dirty area handling to only update precisely the area that was changed

Equbuxu 2 years ago
parent
commit
7f4ba9885a
56 changed files with 1029 additions and 605 deletions
  1. 1 0
      src/ChunkyImageLib/Chunk.cs
  2. 52 36
      src/ChunkyImageLib/ChunkyImage.cs
  3. 107 0
      src/ChunkyImageLib/DataHolders/AffectedArea.cs
  4. 20 0
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  5. 1 1
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  6. 3 3
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  7. 2 2
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  8. 3 2
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  9. 2 2
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  10. 4 3
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  11. 2 2
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  12. 2 2
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  13. 1 1
      src/ChunkyImageLib/Operations/IDrawOperation.cs
  14. 8 2
      src/ChunkyImageLib/Operations/ImageOperation.cs
  15. 45 1
      src/ChunkyImageLib/Operations/OperationHelper.cs
  16. 2 2
      src/ChunkyImageLib/Operations/PathOperation.cs
  17. 2 2
      src/ChunkyImageLib/Operations/PixelOperation.cs
  18. 15 3
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  19. 6 3
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  20. 3 2
      src/ChunkyImageLib/Operations/ReplaceColorOperation.cs
  21. 2 2
      src/ChunkyImageLibTest/ClearRegionOperationTests.cs
  22. 1 1
      src/ChunkyImageLibTest/ImageOperationTests.cs
  23. 14 14
      src/ChunkyImageLibTest/RectangleOperationTests.cs
  24. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/LayerImageArea_ChangeInfo.cs
  25. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/MaskArea_ChangeInfo.cs
  26. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  27. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  28. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  29. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  30. 12 12
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawEllipse_UpdateableChange.cs
  31. 7 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawLine_UpdateableChange.cs
  32. 12 12
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  33. 7 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  34. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  35. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  36. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  37. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PathBasedPen_UpdateableChange.cs
  38. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs
  39. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ReplaceColor_Change.cs
  40. 6 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs
  41. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  42. 10 10
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  43. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  44. 7 2
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  45. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  46. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  47. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  48. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandVisualizer.cs
  49. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs
  50. 107 23
      src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs
  51. 9 5
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  52. 51 36
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  53. 194 0
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  54. 217 0
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  55. 0 310
      src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs
  56. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

+ 1 - 0
src/ChunkyImageLib/Chunk.cs

@@ -94,6 +94,7 @@ public class Chunk : IDisposable
             return;
         returned = true;
         Interlocked.Decrement(ref chunkCounter);
+        Surface.DrawingSurface.Canvas.RestoreToCount(-1);
         ChunkPool.Instance.Push(this);
     }
 }

+ 52 - 36
src/ChunkyImageLib/ChunkyImage.cs

@@ -1,4 +1,5 @@
-using System.Runtime.CompilerServices;
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.CompilerServices;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using OneOf;
@@ -76,7 +77,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
-    private readonly List<(IOperation operation, HashSet<VecI> affectedChunks)> queuedOperations = new();
+    private readonly List<(IOperation operation, AffectedArea affectedArea)> queuedOperations = new();
     private readonly List<ChunkyImage> activeClips = new();
     private BlendMode blendMode = BlendMode.Src;
     private bool lockTransparency = false;
@@ -321,7 +322,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 return true;
             foreach (var operation in queuedOperations)
             {
-                if (operation.affectedChunks.Contains(chunkPos))
+                if (operation.affectedArea.Chunks.Contains(chunkPos))
                     return true;
             }
 
@@ -645,7 +646,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         {
             ThrowIfDisposed();
             ClearOperation operation = new();
-            EnqueueOperation(operation, FindAllChunks());
+            EnqueueOperation(operation, new(FindAllChunks()));
         }
     }
 
@@ -657,7 +658,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             ThrowIfDisposed();
             ResizeOperation operation = new(newSize);
             LatestSize = newSize;
-            EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
+            EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
         }
     }
 
@@ -677,17 +678,18 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
         foreach (var op in operations)
         {
-            var chunks = op.FindAffectedChunks(LatestSize);
-            chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
+            var area = op.FindAffectedArea(LatestSize);
+            area.Chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
+            area.GlobalArea = area.GlobalArea?.Intersect(new RectI(VecI.Zero, LatestSize));
             if (operation.IgnoreEmptyChunks)
-                chunks.IntersectWith(FindAllChunks());
-            EnqueueOperation(op, chunks);
+                area.Chunks.IntersectWith(FindAllChunks());
+            EnqueueOperation(op, area);
         }
     }
 
-    private void EnqueueOperation(IOperation operation, HashSet<VecI> chunks)
+    private void EnqueueOperation(IOperation operation, AffectedArea area)
     {
-        queuedOperations.Add((operation, chunks));
+        queuedOperations.Add((operation, area));
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
@@ -733,9 +735,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         lock (lockObject)
         {
             ThrowIfDisposed();
-            var affectedChunks = FindAffectedChunks();
+            var affectedArea = FindAffectedArea();
 
-            foreach (var chunk in affectedChunks)
+            foreach (var chunk in affectedArea.Chunks)
             {
                 MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
             }
@@ -876,9 +878,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         {
             ThrowIfDisposed();
             var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
-            foreach (var (_, opChunks) in queuedOperations)
+            foreach (var (_, affArea) in queuedOperations)
             {
-                allChunks.UnionWith(opChunks);
+                allChunks.UnionWith(affArea.Chunks);
             }
 
             return allChunks;
@@ -899,19 +901,25 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// Chunks affected by operations that haven't been committed yet
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0)
+    public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
     {
         lock (lockObject)
         {
             ThrowIfDisposed();
             var chunks = new HashSet<VecI>();
+            RectI? rect = null;
+            
             for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
             {
-                var (_, opChunks) = queuedOperations[i];
-                chunks.UnionWith(opChunks);
+                var (_, area) = queuedOperations[i];
+                chunks.UnionWith(area.Chunks);
+
+                rect ??= area.GlobalArea;
+                if (area.GlobalArea is not null && rect is not null)
+                    rect = rect.Value.Union(area.GlobalArea.Value);
             }
 
-            return chunks;
+            return new AffectedArea(chunks, rect);
         }
     }
 
@@ -932,8 +940,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
         for (int i = 0; i < queuedOperations.Count; i++)
         {
-            var (operation, operChunks) = queuedOperations[i];
-            if (!operChunks.Contains(chunkPos))
+            var (operation, affArea) = queuedOperations[i];
+            if (!affArea.Chunks.Contains(chunkPos))
                 continue;
 
             if (!initialized)
@@ -944,7 +952,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             }
 
             if (chunkData.QueueProgress <= i)
-                chunkData.IsDeleted = ApplyOperationToChunk(operation, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
+                chunkData.IsDeleted = ApplyOperationToChunk(operation, affArea, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
         }
 
         if (initialized)
@@ -999,6 +1007,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// </returns>
     private bool ApplyOperationToChunk(
         IOperation operation,
+        AffectedArea operationAffectedArea,
         OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
         Chunk targetChunk,
         VecI chunkPos,
@@ -1010,16 +1019,16 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
         if (operation is IDrawOperation chunkOperation)
         {
-            if (combinedRasterClips.IsT1) //Nothing is visible
+            if (combinedRasterClips.IsT1) // Nothing is visible
                 return chunkData.IsDeleted;
 
             if (chunkData.IsDeleted)
                 targetChunk.Surface.DrawingSurface.Canvas.Clear();
 
             // just regular drawing
-            if (combinedRasterClips.IsT0) //Everything is visible as far as raster clips are concerned
+            if (combinedRasterClips.IsT0) // Everything is visible as far as the raster clips are concerned
             {
-                CallDrawWithClip(chunkOperation, targetChunk, resolution, chunkPos);
+                CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, targetChunk, resolution, chunkPos);
                 return false;
             }
 
@@ -1029,7 +1038,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
             targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
 
-            CallDrawWithClip(chunkOperation, tempChunk, resolution, chunkPos);
+            CallDrawWithClip(chunkOperation, operationAffectedArea.GlobalArea, tempChunk, resolution, chunkPos);
 
             clip.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
             clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
@@ -1046,24 +1055,31 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         return chunkData.IsDeleted;
     }
 
-    private void CallDrawWithClip(IDrawOperation operation, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
+    private void CallDrawWithClip(IDrawOperation operation, RectI? operationAffectedArea, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
     {
+        if (operationAffectedArea is null)
+            return;
+
+        int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
+
+        float scale = (float)resolution.Multiplier();
         if (clippingPath is not null && !clippingPath.IsEmpty)
         {
-            int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
-
             using VectorPath transformedPath = new(clippingPath);
-            float scale = (float)resolution.Multiplier();
             VecD trans = -chunkPos * FullChunkSize * scale;
+            
             transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
             targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
-            operation.DrawOnChunk(targetChunk, chunkPos);
-            targetChunk.Surface.DrawingSurface.Canvas.RestoreToCount(count);
-        }
-        else
-        {
-            operation.DrawOnChunk(targetChunk, chunkPos);
         }
+
+        VecD affectedAreaPos = operationAffectedArea.Value.TopLeft;
+        VecD affectedAreaSize = operationAffectedArea.Value.Size;
+        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);
     }
 
     /// <summary>

+ 107 - 0
src/ChunkyImageLib/DataHolders/AffectedArea.cs

@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace ChunkyImageLib.DataHolders;
+
+/// <summary>
+/// The affected area is defined as the intersection between AffectedArea.Chunks and AffectedArea.GlobalArea. 
+/// In other words, a pixel is considered to be affected when both of those are true:
+/// 1. The pixel falls inside the GlobalArea rectangle;
+/// 2. The Chunks collection contains the chunk that the pixel belongs to.
+/// The GlobalArea == null case is treated as "nothing was affected".
+/// </summary>
+public struct AffectedArea
+{
+    public HashSet<VecI> Chunks { get; set; }
+
+    /// <summary>
+    /// A rectangle in global full-scale coordinat
+    /// </summary>
+    public RectI? GlobalArea { get; set; }
+
+    public AffectedArea()
+    {
+        Chunks = new();
+        GlobalArea = null;
+    }
+
+    public AffectedArea(HashSet<VecI> chunks)
+    {
+        Chunks = chunks;
+        if (chunks.Count == 0)
+        {
+            GlobalArea = null;
+            return;
+        }
+        GlobalArea = new RectI(chunks.First(), new(ChunkyImage.FullChunkSize));
+        foreach (var vec in chunks)
+        {
+            GlobalArea = GlobalArea.Value.Union(new RectI(vec, new(ChunkyImage.FullChunkSize)));
+        }
+    }
+
+    public AffectedArea(HashSet<VecI> chunks, RectI? globalArea)
+    {
+        GlobalArea = globalArea;
+        Chunks = chunks;
+    }
+
+    public AffectedArea(AffectedArea original)
+    {
+        Chunks = new HashSet<VecI>(original.Chunks);
+        GlobalArea = original.GlobalArea;
+    }
+
+    public void UnionWith(AffectedArea other)
+    {
+        Chunks.UnionWith(other.Chunks);
+
+        if (GlobalArea is not null && other.GlobalArea is not null)
+            GlobalArea = GlobalArea.Value.Union(other.GlobalArea.Value);
+        else
+            GlobalArea = GlobalArea ?? other.GlobalArea;
+    }
+
+    public void ExceptWith(HashSet<VecI> otherChunks) => ExceptWith(new AffectedArea(otherChunks));
+
+    public void ExceptWith(AffectedArea other)
+    {
+        Chunks.ExceptWith(other.Chunks);
+        if (GlobalArea is null || other.GlobalArea is null)
+            return;
+
+        RectI overlap = GlobalArea.Value.Intersect(other.GlobalArea.Value);
+
+        if (overlap.IsZeroOrNegativeArea)
+            return;
+
+        if (overlap == other.GlobalArea.Value)
+        {
+            Chunks = new();
+            GlobalArea = null;
+            return;
+        }
+
+        if (overlap.Width == GlobalArea.Value.Width)
+        {
+            if (overlap.Top == GlobalArea.Value.Top)
+                GlobalArea = GlobalArea.Value with { Top = overlap.Bottom };
+            else if (overlap.Bottom == GlobalArea.Value.Bottom)
+                GlobalArea = GlobalArea.Value with { Bottom = overlap.Top };
+            return;
+        }
+
+        if (overlap.Height == GlobalArea.Value.Height)
+        {
+            if (overlap.Left == GlobalArea.Value.Left)
+                GlobalArea = GlobalArea.Value with { Left = overlap.Right };
+            else if (overlap.Right == GlobalArea.Value.Right)
+                GlobalArea = GlobalArea.Value with { Right = overlap.Left };
+            return;
+        }
+    }
+}

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

@@ -91,6 +91,18 @@ public struct ShapeCorners
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
         }
     }
+    public RectD AABBBounds
+    {
+        get
+        {
+            double minX = Math.Min(Math.Min(TopLeft.X, TopRight.X), Math.Min(BottomLeft.X, BottomRight.X));
+            double minY = Math.Min(Math.Min(TopLeft.Y, TopRight.Y), Math.Min(BottomLeft.Y, BottomRight.Y));
+            double maxX = Math.Max(Math.Max(TopLeft.X, TopRight.X), Math.Max(BottomLeft.X, BottomRight.X));
+            double maxY = Math.Max(Math.Max(TopLeft.Y, TopRight.Y), Math.Max(BottomLeft.Y, BottomRight.Y));
+            return RectD.FromTwoPoints(new VecD(minX, minY), new VecD(maxX, maxY));
+        }
+    }
+
     public bool IsPointInside(VecD point)
     {
         var top = TopLeft - TopRight;
@@ -129,4 +141,12 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopRight = TopRight.ReflectX(verAxisX)
     };
+
+    public ShapeCorners AsRotated(double angle, VecD around) => new ShapeCorners
+    {
+        BottomLeft = BottomLeft.Rotate(angle, around),
+        BottomRight = BottomRight.Rotate(angle, around),
+        TopLeft = TopLeft.Rotate(angle, around),
+        TopRight = TopRight.Rotate(angle, around)
+    };
 }

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -14,7 +14,7 @@ public interface IReadOnlyChunkyImage
     Color GetCommittedPixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
-    HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0);
+    AffectedArea FindAffectedArea(int fromOperationIndex = 0);
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindAllChunks();
 }

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

@@ -16,10 +16,10 @@ internal class ApplyMaskOperation : IDrawOperation
     {
         mask = maskToApply;
     }
-    
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return mask.FindCommittedChunks();
+        return new AffectedArea(mask.FindCommittedChunks());
     }
     
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)

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

@@ -38,10 +38,10 @@ internal class BresenhamLineOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
         RectI bounds = RectI.FromTwoPixels(from, to);
-        return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

+ 3 - 2
src/ChunkyImageLib/Operations/ChunkyImageOperation.cs

@@ -92,9 +92,10 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingRectangle(new(GetTopLeft(), imageToDraw.CommittedSize), ChunkyImage.FullChunkSize);
+        RectI rect = new(GetTopLeft(), imageToDraw.CommittedSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(rect, ChunkyImage.FullChunkSize), rect);
     }
 
     private VecI GetTopLeft()

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

@@ -29,9 +29,9 @@ internal class ClearPathOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingRectangle(pathTightBounds, ChunkPool.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(pathTightBounds, ChunkPool.FullChunkSize), pathTightBounds);
     }
     public void Dispose()
     {

+ 4 - 3
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -1,4 +1,5 @@
-using PixiEditor.DrawingApi.Core.Numerics;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace ChunkyImageLib.Operations;
 
@@ -24,9 +25,9 @@ internal class ClearRegionOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingRectangle(rect, ChunkPool.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(rect, ChunkPool.FullChunkSize), rect);
     }
     public void Dispose() { }
 

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

@@ -38,10 +38,10 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
         RectI bounds = RectI.FromTwoPoints(from, to).Inflate((int)Math.Ceiling(paint.StrokeWidth));
-        return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

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

@@ -93,7 +93,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
         var chunks = OperationHelper.FindChunksTouchingEllipse
             (location.Center, location.Width / 2.0, location.Height / 2.0, ChunkyImage.FullChunkSize);
@@ -102,7 +102,7 @@ internal class EllipseOperation : IMirroredDrawOperation
             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
                 (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize));
         }
-        return chunks;
+        return new AffectedArea(chunks, location);
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

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

@@ -7,5 +7,5 @@ internal interface IDrawOperation : IOperation
 {
     bool IgnoreEmptyChunks { get; }
     void DrawOnChunk(Chunk chunk, VecI chunkPos);
-    HashSet<VecI> FindAffectedChunks(VecI imageSize);
+    AffectedArea FindAffectedArea(VecI imageSize);
 }

+ 8 - 2
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -94,9 +94,9 @@ internal class ImageOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingQuadrilateral(corners, ChunkPool.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingQuadrilateral(corners, ChunkPool.FullChunkSize), (RectI)corners.AABBBounds.RoundOutwards());
     }
 
     public void Dispose()
@@ -109,14 +109,20 @@ internal class ImageOperation : IMirroredDrawOperation
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
     {
         if (verAxisX is not null && horAxisY is not null)
+        {
             return new ImageOperation
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+        }
         if (verAxisX is not null)
+        {
             return new ImageOperation
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
+        }
         if (horAxisY is not null)
+        {
             return new ImageOperation
                 (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+        }
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
     }
 }

+ 45 - 1
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -21,8 +21,14 @@ public static class OperationHelper
     /// <summary>
     /// toModify[x,y].Alpha = Math.Min(toModify[x,y].Alpha, toGetAlphaFrom[x,y].Alpha)
     /// </summary>
-    public unsafe static void ClampAlpha(DrawingSurface toModify, DrawingSurface toGetAlphaFrom)
+    public static unsafe void ClampAlpha(DrawingSurface toModify, DrawingSurface toGetAlphaFrom, RectI? clippingRect = null)
     {
+        if (clippingRect is not null)
+        {
+            ClampAlphaWithClippingRect(toModify, toGetAlphaFrom, (RectI)clippingRect);
+            return;
+        }
+
         using Pixmap map = toModify.PeekPixels();
         using Pixmap refMap = toGetAlphaFrom.PeekPixels();
         long* pixels = (long*)map.GetPixels();
@@ -52,6 +58,44 @@ public static class OperationHelper
         }
     }
 
+    private static unsafe void ClampAlphaWithClippingRect(DrawingSurface toModify, DrawingSurface toGetAlphaFrom, RectI clippingRect)
+    {
+        using Pixmap map = toModify.PeekPixels();
+        using Pixmap refMap = toGetAlphaFrom.PeekPixels();
+        long* pixels = (long*)map.GetPixels();
+        long* refPixels = (long*)refMap.GetPixels();
+        int size = map.Width * map.Height;
+        if (map.Width != refMap.Width || map.Height != refMap.Height)
+            throw new ArgumentException("The surfaces must have the same size");
+        RectI workingArea = clippingRect.Intersect(new RectI(0, 0, map.Width, map.Height));
+        if (workingArea.IsZeroOrNegativeArea)
+            return;
+
+        for (int y = workingArea.Top; y < workingArea.Bottom; y++)
+        {
+            for (int x = workingArea.Left; x < workingArea.Right; x++)
+            {
+                int position = x + y * map.Width;
+                long* offset = pixels + position;
+                long* refOffset = refPixels + position;
+                Half* alpha = (Half*)offset + 3;
+                Half* refAlpha = (Half*)refOffset + 3;
+                if (*refAlpha < *alpha)
+                {
+                    float a = (float)(*alpha);
+                    float r = (float)(*((Half*)offset)) / a;
+                    float g = (float)(*((Half*)offset + 1)) / a;
+                    float b = (float)(*((Half*)offset + 2)) / a;
+                    float newA = (float)(*refAlpha);
+                    Half newR = (Half)(r * newA);
+                    Half newG = (Half)(g * newA);
+                    Half newB = (Half)(b * newA);
+                    *offset = (*(ushort*)(&newR)) | ((long)*(ushort*)(&newG)) << 16 | ((long)*(ushort*)(&newB)) << 32 | ((long)*(ushort*)(refAlpha)) << 48;
+                }
+            }
+        }
+    }
+
     public static ShapeCorners ConvertForResolution(ShapeCorners corners, ChunkResolution resolution)
     {
         return new ShapeCorners()

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

@@ -35,9 +35,9 @@ internal class PathOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

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

@@ -35,9 +35,9 @@ internal class PixelOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) };
+        return new AffectedArea(new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) }, new RectI(pixel, VecI.One));
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

+ 15 - 3
src/ChunkyImageLib/Operations/PixelsOperation.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib.DataHolders;
+using System.ComponentModel.DataAnnotations.Schema;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
@@ -35,9 +36,20 @@ internal class PixelsOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return pixels.Select(static pixel => OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize)).ToHashSet();
+        HashSet<VecI> affectedChunks = new HashSet<VecI>();
+        RectI? affectedArea = null;
+        foreach (var pixel in pixels)
+        {
+            affectedChunks.Add(OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize));
+            if (affectedArea is null)
+                affectedArea = new RectI(pixel, VecI.One);
+            else
+                affectedArea = affectedArea.Value.Union(new RectI(pixel, VecI.One));
+        }
+
+        return new AffectedArea(affectedChunks, affectedArea);
     }
 
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)

+ 6 - 3
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -50,12 +50,15 @@ internal class RectangleOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
         if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 || (Data.StrokeColor.A == 0 && Data.FillColor.A == 0))
             return new();
+
+        RectI affRect = (RectI)new ShapeCorners(Data.Center, Data.Size).AsRotated(Data.Angle, Data.Center).AABBBounds.RoundOutwards();
+
         if (Data.FillColor.A != 0 || Math.Abs(Data.Size.X) == 1 || Math.Abs(Data.Size.Y) == 1)
-            return OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize);
+            return new (OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize), affRect);
 
         var chunks = OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize);
         chunks.ExceptWith(
@@ -64,7 +67,7 @@ internal class RectangleOperation : IMirroredDrawOperation
                 Data.Size.Abs() - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
                 Data.Angle,
                 ChunkPool.FullChunkSize));
-        return chunks;
+        return new (chunks, affRect);
     }
 
     public void Dispose() { }

+ 3 - 2
src/ChunkyImageLib/Operations/ReplaceColorOperation.cs

@@ -46,9 +46,10 @@ internal class ReplaceColorOperation : IDrawOperation
         }
     }
 
-    HashSet<VecI> IDrawOperation.FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
-        return OperationHelper.FindChunksTouchingRectangle(new RectI(VecI.Zero, imageSize), ChunkyImage.FullChunkSize);
+        RectI rect = new(VecI.Zero, imageSize);
+        return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(rect, ChunkyImage.FullChunkSize), rect);
     }
 
     public void Dispose()

+ 2 - 2
src/ChunkyImageLibTest/ClearRegionOperationTests.cs

@@ -15,7 +15,7 @@ public class ClearRegionOperationTests
     {
         ClearRegionOperation operation = new(new(new(chunkSize, chunkSize), new(chunkSize, chunkSize)));
         var expected = new HashSet<VecI>() { new(1, 1) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
         Assert.Equal(expected, actual);
     }
 
@@ -34,7 +34,7 @@ public class ClearRegionOperationTests
             new(-2, -0), new(-1, -0), new(0, -0), new(1, -0),
             new(-2,  1), new(-1,  1), new(0,  1), new(1,  1),
         };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
         Assert.Equal(expected, actual);
     }
 #pragma warning restore format

+ 1 - 1
src/ChunkyImageLibTest/ImageOperationTests.cs

@@ -13,7 +13,7 @@ public class ImageOperationTests
     {
         using Surface testImage = new Surface((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         using ImageOperation operation = new((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), testImage);
-        var chunks = operation.FindAffectedChunks(new(ChunkyImage.FullChunkSize));
+        var chunks = operation.FindAffectedArea(new(ChunkyImage.FullChunkSize)).Chunks;
         Assert.Equal(new HashSet<VecI>() { new(1, 1) }, chunks);
     }
 }

+ 14 - 14
src/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -13,31 +13,31 @@ public class RectangleOperationTests
 // to keep expected rectangles aligned
 #pragma warning disable format
     [Fact]
-    public void FindAffectedChunks_SmallStrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_SmallStrokeOnly_FindsCorrectChunks()
     {
         var (x, y, w, h) = (chunkSize / 2, chunkSize / 2, chunkSize, chunkSize);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
 
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_2by2StrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_2by2StrokeOnly_FindsCorrectChunks()
     {
         var (x, y, w, h) = (0, 0, chunkSize * 2, chunkSize * 2);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
 
         HashSet<VecI> expected = new() { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_3x3PositiveStrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_3x3PositiveStrokeOnly_FindsCorrectChunks()
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
@@ -48,13 +48,13 @@ public class RectangleOperationTests
             new(1, 2),            new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
         };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_3x3NegativeStrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_3x3NegativeStrokeOnly_FindsCorrectChunks()
     {
         var (x, y, w, h) = (-chunkSize * 2 - chunkSize / 2, -chunkSize * 2 - chunkSize / 2, chunkSize * 2, chunkSize * 2);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
@@ -65,13 +65,13 @@ public class RectangleOperationTests
             new(-4, -3),              new(-2, -3),
             new(-4, -2), new(-3, -2), new(-2, -2),
         };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_3x3PositiveFilled_FindsCorrectChunks()
+    public void FindAffectedArea_3x3PositiveFilled_FindsCorrectChunks()
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.White));
@@ -82,13 +82,13 @@ public class RectangleOperationTests
             new(1, 2), new(2, 2), new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
         };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_ThickPositiveStroke_FindsCorrectChunks()
+    public void FindAffectedArea_ThickPositiveStroke_FindsCorrectChunks()
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 4, chunkSize * 4);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, Colors.Black, Colors.Transparent));
@@ -101,19 +101,19 @@ public class RectangleOperationTests
             new(0, 3), new(1, 3), new(2, 3), new(3, 3), new(4, 3),
             new(0, 4), new(1, 4), new(2, 4), new(3, 4), new(4, 4),
         };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }
 
     [Fact]
-    public void FindAffectedChunks_SmallButThick_FindsCorrectChunks()
+    public void FindAffectedArea_SmallButThick_FindsCorrectChunks()
     {
         var (x, y, w, h) = (chunkSize / 2f - 0.5, chunkSize / 2f - 0.5, 1, 1);
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, Colors.Black, Colors.White));
 
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
         Assert.Equal(expected, actual);
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/LayerImageChunks_ChangeInfo.cs → src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/LayerImageArea_ChangeInfo.cs

@@ -2,4 +2,4 @@
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 
-public record class LayerImageChunks_ChangeInfo(Guid GuidValue, HashSet<VecI> Chunks) : IChangeInfo;
+public record class LayerImageArea_ChangeInfo(Guid GuidValue, AffectedArea Area) : IChangeInfo;

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/MaskChunks_ChangeInfo.cs → src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/MaskArea_ChangeInfo.cs

@@ -2,4 +2,4 @@
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 
-public record class MaskChunks_ChangeInfo(Guid GuidValue, HashSet<VecI> Chunks) : IChangeInfo;
+public record class MaskArea_ChangeInfo(Guid GuidValue, AffectedArea Area) : IChangeInfo;

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

@@ -49,7 +49,7 @@ internal class ApplyLayerMask_Change : Change
         return new List<IChangeInfo>
         {
             new StructureMemberMask_ChangeInfo(layerGuid, false),
-            new LayerImageChunks_ChangeInfo(layerGuid, affectedChunks)
+            new LayerImageArea_ChangeInfo(layerGuid, new AffectedArea(affectedChunks))
         };
     }
 
@@ -63,19 +63,19 @@ internal class ApplyLayerMask_Change : Change
 
         ChunkyImage newMask = new ChunkyImage(target.Size);
         savedMask.ApplyChunksToImage(newMask);
-        var affectedChunksMask = newMask.FindAffectedChunks();
+        var affectedChunksMask = newMask.FindAffectedArea();
         newMask.CommitChanges();
         layer.Mask = newMask;
 
         savedLayer.ApplyChunksToImage(layer.LayerImage);
-        var affectedChunksLayer = layer.LayerImage.FindAffectedChunks();
+        var affectedChunksLayer = layer.LayerImage.FindAffectedArea();
         layer.LayerImage.CommitChanges();
 
         return new List<IChangeInfo>
         {
             new StructureMemberMask_ChangeInfo(layerGuid, true),
-            new LayerImageChunks_ChangeInfo(layerGuid, affectedChunksLayer),
-            new MaskChunks_ChangeInfo(layerGuid, affectedChunksMask)
+            new LayerImageArea_ChangeInfo(layerGuid, affectedChunksLayer),
+            new MaskArea_ChangeInfo(layerGuid, affectedChunksMask)
         };
     }
 

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -60,9 +60,9 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
         
         ChangeBrightness(ellipseLines, strokeWidth, pos + new VecI(-strokeWidth / 2), correctionFactor, repeat, tempSurface, layer.LayerImage);
         
-        var affected = layer.LayerImage.FindAffectedChunks(queueLength);
+        var affected = layer.LayerImage.FindAffectedArea(queueLength);
         
-        return new LayerImageChunks_ChangeInfo(layerGuid, affected);
+        return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
     
     private static void ChangeBrightness(
@@ -112,18 +112,18 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             }
         }
 
-        var affChunks = layer.LayerImage.FindAffectedChunks();
-        savedChunks = new CommittedChunkStorage(layer.LayerImage, affChunks);
+        var affArea = layer.LayerImage.FindAffectedArea();
+        savedChunks = new CommittedChunkStorage(layer.LayerImage, affArea.Chunks);
         layer.LayerImage.CommitChanges();
         if (firstApply)
             return new None();
-        return new LayerImageChunks_ChangeInfo(layerGuid, affChunks);
+        return new LayerImageArea_ChangeInfo(layerGuid, affArea);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, layerGuid, false, ref savedChunks);
-        return new LayerImageChunks_ChangeInfo(layerGuid, affected);
+        return new LayerImageArea_ChangeInfo(layerGuid, affected);
     }
 
     public override void Dispose()

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs

@@ -30,17 +30,17 @@ internal class ClearSelectedArea_Change : Change
         RectI intBounds = (RectI)bounds.Intersect(new RectD(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
 
         image.EnqueueClearPath(target.Selection.SelectionPath, intBounds);
-        var affChunks = image.FindAffectedChunks();
-        savedChunks = new(image, affChunks);
+        var affArea = image.FindAffectedArea();
+        savedChunks = new(image, affArea.Chunks);
         image.CommitChanges();
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affectedChunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref savedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        var affArea = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref savedChunks);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override void Dispose()

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -69,19 +69,19 @@ internal class CombineStructureMembersOnto_Change : Change
                 combined.AsT0.Dispose();
             }
         }
-        var affectedChunks = toDrawOn.LayerImage.FindAffectedChunks();
-        originalChunks = new CommittedChunkStorage(toDrawOn.LayerImage, affectedChunks);
+        var affArea = toDrawOn.LayerImage.FindAffectedArea();
+        originalChunks = new CommittedChunkStorage(toDrawOn.LayerImage, affArea.Chunks);
         toDrawOn.LayerImage.CommitChanges();
 
         ignoreInUndo = false;
-        return new LayerImageChunks_ChangeInfo(targetLayer, affectedChunks);
+        return new LayerImageArea_ChangeInfo(targetLayer, affArea);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var toDrawOn = target.FindMemberOrThrow<Layer>(targetLayer);
-        var affectedChunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(toDrawOn.LayerImage, ref originalChunks);
-        return new LayerImageChunks_ChangeInfo(targetLayer, affectedChunks);
+        var affectedArea = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(toDrawOn.LayerImage, ref originalChunks);
+        return new LayerImageArea_ChangeInfo(targetLayer, affectedArea);
     }
 
     public override void Dispose()

+ 12 - 12
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawEllipse_UpdateableChange.cs

@@ -35,9 +35,9 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
     }
 
-    private HashSet<VecI> UpdateEllipse(Document target, ChunkyImage targetImage)
+    private AffectedArea UpdateEllipse(Document target, ChunkyImage targetImage)
     {
-        var oldAffectedChunks = targetImage.FindAffectedChunks();
+        var oldAffectedChunks = targetImage.FindAffectedArea();
 
         targetImage.CancelChanges();
 
@@ -47,10 +47,10 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
             targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth);
         }
 
-        var affectedChunks = targetImage.FindAffectedChunks();
-        affectedChunks.UnionWith(oldAffectedChunks);
+        var affectedArea = targetImage.FindAffectedArea();
+        affectedArea.UnionWith(oldAffectedChunks);
 
-        return affectedChunks;
+        return affectedArea;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -62,24 +62,24 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
         }
 
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var chunks = UpdateEllipse(target, image);
-        storedChunks = new CommittedChunkStorage(image, image.FindAffectedChunks());
+        var area = UpdateEllipse(target, image);
+        storedChunks = new CommittedChunkStorage(image, image.FindAffectedArea().Chunks);
         image.CommitChanges();
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var chunks = UpdateEllipse(target, image);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        var area = UpdateEllipse(target, image);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affectedChunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
-        var changes = DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        var affArea = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
+        var changes = DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         return changes;
     }
 

+ 7 - 7
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawLine_UpdateableChange.cs

@@ -43,10 +43,10 @@ internal class DrawLine_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
     }
 
-    private HashSet<VecI> CommonApply(Document target)
+    private AffectedArea CommonApply(Document target)
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var oldAffected = image.FindAffectedChunks();
+        var oldAffected = image.FindAffectedArea();
         image.CancelChanges();
         if (from != to)
         {
@@ -56,14 +56,14 @@ internal class DrawLine_UpdateableChange : UpdateableChange
             else
                 image.EnqueueDrawSkiaLine(from, to, caps, strokeWidth, color, BlendMode.SrcOver);
         }
-        var totalAffected = image.FindAffectedChunks();
+        var totalAffected = image.FindAffectedArea();
         totalAffected.UnionWith(oldAffected);
         return totalAffected;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, CommonApply(target), drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, CommonApply(target), drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -78,18 +78,18 @@ internal class DrawLine_UpdateableChange : UpdateableChange
         var affected = CommonApply(target);
         if (savedChunks is not null)
             throw new InvalidOperationException("Trying to save chunks while there are saved chunks already");
-        savedChunks = new CommittedChunkStorage(image, image.FindAffectedChunks());
+        savedChunks = new CommittedChunkStorage(image, image.FindAffectedArea().Chunks);
         image.CommitChanges();
 
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull
             (target, memberGuid, drawOnMask, ref savedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 
     public override void Dispose()

+ 12 - 12
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs

@@ -28,9 +28,9 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
         rect = rectangle;
     }
 
-    private HashSet<VecI> UpdateRectangle(Document target, ChunkyImage targetImage)
+    private AffectedArea UpdateRectangle(Document target, ChunkyImage targetImage)
     {
-        var oldAffectedChunks = targetImage.FindAffectedChunks();
+        var oldAffArea = targetImage.FindAffectedArea();
 
         targetImage.CancelChanges();
 
@@ -40,17 +40,17 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
             targetImage.EnqueueDrawRectangle(rect);
         }
 
-        var affectedChunks = targetImage.FindAffectedChunks();
-        affectedChunks.UnionWith(oldAffectedChunks);
+        var affArea = targetImage.FindAffectedArea();
+        affArea.UnionWith(oldAffArea);
 
-        return affectedChunks;
+        return affArea;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var chunks = UpdateRectangle(target, targetImage);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        var area = UpdateRectangle(target, targetImage);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -62,18 +62,18 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
         }
 
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var affectedChunks = UpdateRectangle(target, targetImage);
-        storedChunks = new CommittedChunkStorage(targetImage, affectedChunks);
+        var area = UpdateRectangle(target, targetImage);
+        storedChunks = new CommittedChunkStorage(targetImage, area.Chunks);
         targetImage.CommitChanges();
 
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affectedChunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        var area = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
 
     public override void Dispose()

+ 7 - 7
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -3,22 +3,22 @@
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal static class DrawingChangeHelper
 {
-    public static HashSet<VecI> ApplyStoredChunksDisposeAndSetToNull(Document target, Guid memberGuid, bool drawOnMask, ref CommittedChunkStorage? storage)
+    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(Document target, Guid memberGuid, bool drawOnMask, ref CommittedChunkStorage? storage)
     {
         var image = GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         return ApplyStoredChunksDisposeAndSetToNull(image, ref storage);
     }
 
-    public static HashSet<VecI> ApplyStoredChunksDisposeAndSetToNull(ChunkyImage image, ref CommittedChunkStorage? storage)
+    public static AffectedArea ApplyStoredChunksDisposeAndSetToNull(ChunkyImage image, ref CommittedChunkStorage? storage)
     {
         if (storage is null)
             throw new InvalidOperationException("No stored chunks to apply");
         storage.ApplyChunksToImage(image);
-        var chunks = image.FindAffectedChunks();
+        var area = image.FindAffectedArea();
         image.CommitChanges();
         storage.Dispose();
         storage = null;
-        return chunks;
+        return area;
     }
 
     public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask)
@@ -72,10 +72,10 @@ internal static class DrawingChangeHelper
         };
     }
 
-    public static OneOf<None, IChangeInfo, List<IChangeInfo>> CreateChunkChangeInfo(Guid memberGuid, HashSet<VecI> affectedChunks, bool drawOnMask) =>
+    public static OneOf<None, IChangeInfo, List<IChangeInfo>> CreateAreaChangeInfo(Guid memberGuid, AffectedArea affectedArea, bool drawOnMask) =>
         drawOnMask switch
         {
-            false => new LayerImageChunks_ChangeInfo(memberGuid, affectedChunks),
-            true => new MaskChunks_ChangeInfo(memberGuid, affectedChunks),
+            false => new LayerImageArea_ChangeInfo(memberGuid, affectedArea),
+            true => new MaskArea_ChangeInfo(memberGuid, affectedArea),
         };
 }

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -52,20 +52,20 @@ internal class FloodFill_Change : Change
         {
             image.EnqueueDrawImage(chunkPos * ChunkyImage.FullChunkSize, chunk.Surface, null, false);
         }
-        var affectedChunks = image.FindAffectedChunks();
-        chunkStorage = new CommittedChunkStorage(image, affectedChunks);
+        var affArea = image.FindAffectedArea();
+        chunkStorage = new CommittedChunkStorage(image, affArea.Chunks);
         image.CommitChanges();
         foreach (var chunk in floodFilledChunks.Values)
             chunk.Dispose();
 
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affectedChunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref chunkStorage);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        var affArea = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref chunkStorage);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override void Dispose()

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -64,9 +64,9 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             image.EnqueueDrawEllipse(rect, color, color, 1, srcPaint);
             image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
         }
-        var affChunks = image.FindAffectedChunks(opCount);
+        var affChunks = image.FindAffectedArea(opCount);
 
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
     }
 
     private void FastforwardEnqueueDrawLines(ChunkyImage targetImage)
@@ -112,11 +112,11 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         ignoreInUndo = false;
         if (firstApply)
         {
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
         else
         {
@@ -125,18 +125,18 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
             FastforwardEnqueueDrawLines(image);
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 
     public override void Dispose()

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -36,9 +36,9 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         this.corners = corners;
     }
 
-    private HashSet<VecI> DrawImage(Document target, ChunkyImage targetImage)
+    private AffectedArea DrawImage(Document target, ChunkyImage targetImage)
     {
-        var prevChunks = targetImage.FindAffectedChunks();
+        var prevAffArea = targetImage.FindAffectedArea();
 
         targetImage.CancelChanges();
         if (!ignoreClipsSymmetriesEtc)
@@ -46,9 +46,9 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         targetImage.EnqueueDrawImage(corners, imageToPaste, RegularPaint, false);
         hasEnqueudImage = true;
 
-        var affectedChunks = targetImage.FindAffectedChunks();
-        affectedChunks.UnionWith(prevChunks);
-        return affectedChunks;
+        var affArea = targetImage.FindAffectedArea();
+        affArea.UnionWith(prevAffArea);
+        return affArea;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -56,23 +56,23 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var chunks = DrawImage(target, targetImage);
         savedChunks?.Dispose();
-        savedChunks = new(targetImage, targetImage.FindAffectedChunks());
+        savedChunks = new(targetImage, targetImage.FindAffectedArea().Chunks);
         targetImage.CommitChanges();
         hasEnqueudImage = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, chunks, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, DrawImage(target, targetImage), drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, DrawImage(target, targetImage), drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref savedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, chunks, drawOnMask);
     }
 
     public override void Dispose()

+ 9 - 9
src/PixiEditor.ChangeableDocument/Changes/Drawing/PathBasedPen_UpdateableChange.cs

@@ -111,11 +111,11 @@ internal class PathBasedPen_UpdateableChange : UpdateableChange
             UpdateTempPathFinish();
 
             image.EnqueueDrawPath(tempPath, color, strokeWidth, StrokeCap.Round, BlendMode.Src);
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
         else
         {
@@ -123,11 +123,11 @@ internal class PathBasedPen_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
             FastforwardEnqueueDrawPath(image);
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
     }
 
@@ -138,15 +138,15 @@ internal class PathBasedPen_UpdateableChange : UpdateableChange
 
         int opCount = image.QueueLength;
         image.EnqueueDrawPath(tempPath, color, strokeWidth, StrokeCap.Round, BlendMode.Src);
-        var affChunks = image.FindAffectedChunks(opCount);
+        var affArea = image.FindAffectedArea(opCount);
 
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 
     public override void Dispose()

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs

@@ -93,8 +93,8 @@ internal class PixelPerfectPen_UpdateableChange : UpdateableChange
 
         int changeCount = image.QueueLength;
         DoDrawingIteration(image, incomingPoints!.Count);
-        HashSet<VecI> affChunks = image.FindAffectedChunks(changeCount);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        var affArea = image.FindAffectedArea(changeCount);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -117,15 +117,15 @@ internal class PixelPerfectPen_UpdateableChange : UpdateableChange
             image.EnqueueDrawPixels(confirmedPixels, color, BlendMode.Src);
         }
 
-        var affChunks = image.FindAffectedChunks();
-        chunkStorage = new CommittedChunkStorage(image, affChunks);
+        var affArea = image.FindAffectedArea();
+        chunkStorage = new CommittedChunkStorage(image, affArea.Chunks);
         image.CommitChanges();
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref chunkStorage);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, chunks, drawOnMask);
     }
 }

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/ReplaceColor_Change.cs

@@ -32,11 +32,11 @@ internal class ReplaceColor_Change : Change
             if (member is not Layer layer)
                 return;
             layer.LayerImage.EnqueueReplaceColor(oldColor, newColor);
-            HashSet<VecI>? chunks = layer.LayerImage.FindAffectedChunks();
-            CommittedChunkStorage storage = new(layer.LayerImage, chunks);
+            var affArea = layer.LayerImage.FindAffectedArea();
+            CommittedChunkStorage storage = new(layer.LayerImage, affArea.Chunks);
             savedChunks[layer.GuidValue] = storage;
             layer.LayerImage.CommitChanges();
-            infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks));
+            infos.Add(new LayerImageArea_ChangeInfo(layer.GuidValue, affArea));
         });
         ignoreInUndo = !savedChunks.Any();
         return infos;
@@ -52,8 +52,8 @@ internal class ReplaceColor_Change : Change
             if (member is not Layer layer)
                 return;
             CommittedChunkStorage? storage = savedChunks[member.GuidValue];
-            var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(layer.LayerImage, ref storage);
-            infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks));
+            var affArea = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(layer.LayerImage, ref storage);
+            infos.Add(new LayerImageArea_ChangeInfo(layer.GuidValue, affArea));
         });
         savedChunks = null;
         return infos;

+ 6 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs

@@ -4,16 +4,17 @@ namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal static class ShiftLayerHelper
 {
-    public static HashSet<VecI> DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta)
+    public static AffectedArea DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta)
     {
         var targetImage = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
-        var prevChunks = targetImage.FindAffectedChunks();
+        var prevArea = targetImage.FindAffectedArea();
         targetImage.CancelChanges();
         if (!keepOriginal)
             targetImage.EnqueueClear();
         targetImage.EnqueueDrawChunkyImage(delta, targetImage, false, false);
-        var curChunks = targetImage.FindAffectedChunks();
-        curChunks.UnionWith(prevChunks);
-        return curChunks;
+        var curArea = targetImage.FindAffectedArea();
+
+        curArea.UnionWith(prevArea);
+        return curArea;
     }
 }

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs

@@ -49,12 +49,12 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         {
-            var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta);
+            var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta);
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, chunks));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
             
-            originalLayerChunks[layerGuid] = new(image, image.FindAffectedChunks());
+            originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
             image.CommitChanges();
         }
 
@@ -69,7 +69,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         foreach (var layerGuid in layerGuids)
         {
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta);
-            _tempChanges.Add(new LayerImageChunks_ChangeInfo(layerGuid, chunks));
+            _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
         
         return _tempChanges;
@@ -83,7 +83,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, affected));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         
         return changes;

+ 10 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -101,9 +101,9 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
     }
 
-    private HashSet<VecI> DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
+    private AffectedArea DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
     {
-        var prevChunks = memberImage.FindAffectedChunks();
+        var prevAffArea = memberImage.FindAffectedArea();
 
         memberImage.CancelChanges();
 
@@ -114,9 +114,9 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
         hasEnqueudImages = true;
 
-        var affectedChunks = memberImage.FindAffectedChunks();
-        affectedChunks.UnionWith(prevChunks);
-        return affectedChunks;
+        var affectedArea = memberImage.FindAffectedArea();
+        affectedArea.UnionWith(prevAffArea);
+        return affectedArea;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -129,10 +129,10 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         {
             ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
-            var chunks = DrawImage(target, guid, image, pos, memberImage);
-            savedChunks[guid] = new(memberImage, memberImage.FindAffectedChunks());
+            var area = DrawImage(target, guid, image, pos, memberImage);
+            savedChunks[guid] = new(memberImage, memberImage.FindAffectedArea().Chunks);
             memberImage.CommitChanges();
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, area, drawOnMask).AsT1);
         }
 
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
@@ -148,7 +148,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         {
             ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
         }
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
         return infos;
@@ -161,7 +161,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         {
             var storageCopy = storage;
             var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, ref storageCopy);
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, chunks, drawOnMask).AsT1);
         }
 
         (var toDispose, target.Selection.SelectionPath) = (target.Selection.SelectionPath, new VectorPath(originalPath!));

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs

@@ -68,9 +68,9 @@ internal class CenterContent_Change : Change
         {
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, shift);
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, chunks));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
             
-            originalLayerChunks[layerGuid] = new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedChunks());
+            originalLayerChunks[layerGuid] = new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindAffectedArea().Chunks);
             layer.LayerImage.CommitChanges();
         }
 
@@ -86,7 +86,7 @@ internal class CenterContent_Change : Change
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, affected));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         
         return changes;

+ 7 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -74,7 +74,10 @@ internal sealed class FlipImage_Change : Change
         bool flipY = flipType == FlipType.Vertical;
         
         flipped.DrawingSurface.Canvas.Save();
-                flipped.DrawingSurface.Canvas.Scale(flipX ? -1 : 1, flipY ? -1 : 1, flipX ? bounds.X + (bounds.Width / 2f) : 0,
+        flipped.DrawingSurface.Canvas.Scale(
+            flipX ? -1 : 1, 
+            flipY ? -1 : 1, 
+            flipX ? bounds.X + (bounds.Width / 2f) : 0,
             flipY ? bounds.Y + (bounds.Height / 2f) : 0f);
         flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
         flipped.DrawingSurface.Canvas.Restore();
@@ -100,13 +103,15 @@ internal sealed class FlipImage_Change : Change
                 {
                     FlipImage(layer.LayerImage);
                     changes.Add(
-                        new LayerImageChunks_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                        new LayerImageArea_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedArea()));
                     layer.LayerImage.CommitChanges();
                 }
 
                 if (member.Mask is not null)
                 {
                     FlipImage(member.Mask);
+                    changes.Add(
+                        new MaskArea_ChangeInfo(member.GuidValue, member.Mask.FindAffectedArea()));
                     member.Mask.CommitChanges();
                 }
             }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -25,7 +25,7 @@ internal abstract class ResizeBasedChangeBase : Change
         img.EnqueueClear();
         img.EnqueueDrawChunkyImage(offset, img);
 
-        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, img.FindAffectedChunks()));
+        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, img.FindAffectedArea().Chunks));
         img.CommitChanges();
     }
     

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -91,15 +91,15 @@ internal class ResizeImage_Change : Change
             if (member is Layer layer)
             {
                 ScaleChunkyImage(layer.LayerImage);
-                var affected = layer.LayerImage.FindAffectedChunks();
-                savedChunks[layer.GuidValue] = new CommittedChunkStorage(layer.LayerImage, affected);
+                var affected = layer.LayerImage.FindAffectedArea();
+                savedChunks[layer.GuidValue] = new CommittedChunkStorage(layer.LayerImage, affected.Chunks);
                 layer.LayerImage.CommitChanges();
             }
             if (member.Mask is not null)
             {
                 ScaleChunkyImage(member.Mask);
-                var affected = member.Mask.FindAffectedChunks();
-                savedMaskChunks[member.GuidValue] = new CommittedChunkStorage(member.Mask, affected);
+                var affected = member.Mask.FindAffectedArea();
+                savedMaskChunks[member.GuidValue] = new CommittedChunkStorage(member.Mask, affected.Chunks);
                 member.Mask.CommitChanges();
             }
         });

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -115,9 +115,9 @@ internal sealed class RotateImage_Change : Change
         img.EnqueueClear();
         img.EnqueueDrawImage(bounds.Pos, flipped);
 
-        var affectedChunks = img.FindAffectedChunks();
-        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, affectedChunks));
-        changes?.Add(new LayerImageChunks_ChangeInfo(memberGuid, affectedChunks));
+        var affArea = img.FindAffectedArea();
+        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, affArea.Chunks));
+        changes?.Add(new LayerImageArea_ChangeInfo(memberGuid, affArea));
         img.CommitChanges();
     }
 
@@ -215,7 +215,7 @@ internal sealed class RotateImage_Change : Change
             {
                 layer.LayerImage.EnqueueResize(originalSize);
                 deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
-                revertChanges.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                revertChanges.Add(new LayerImageArea_ChangeInfo(layer.GuidValue, layer.LayerImage.FindAffectedArea()));
                 layer.LayerImage.CommitChanges();
             }
 
@@ -223,7 +223,7 @@ internal sealed class RotateImage_Change : Change
                 return;
             member.Mask.EnqueueResize(originalSize);
             deletedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
-            revertChanges.Add(new LayerImageChunks_ChangeInfo(member.GuidValue, member.Mask.FindAffectedChunks()));
+            revertChanges.Add(new LayerImageArea_ChangeInfo(member.GuidValue, member.Mask.FindAffectedArea()));
             member.Mask.CommitChanges();
         });
 

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

@@ -43,8 +43,10 @@ internal class MagicWandVisualizer
             surface.Canvas.Clear(Colors.White);
             if (previousImage != null)
             {
-                surface.Canvas.DrawImage(previousImage,
-                    RectD.Create(VecI.Zero, new VecI(previousImage.Width, previousImage.Height)), replacementPaint);
+                surface.Canvas.DrawImage(
+                    previousImage,
+                    RectD.Create(VecI.Zero, new VecI(previousImage.Width, previousImage.Height)), 
+                    replacementPaint);
             }
 
             var scaledStart = new VecI(step.Start.X * (width / originalWidth), step.Start.Y * (height / originalHeight));

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs

@@ -30,8 +30,8 @@ internal sealed class ApplyMask_Change : Change
         var layer = (Layer)target.FindMember(structureMemberGuid)!;
         layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
         ignoreInUndo = false;
-        var layerInfo = new LayerImageChunks_ChangeInfo(structureMemberGuid, layer.LayerImage.FindAffectedChunks());
-        savedChunks = new CommittedChunkStorage(layer.LayerImage, layerInfo.Chunks);
+        var layerInfo = new LayerImageArea_ChangeInfo(structureMemberGuid, layer.LayerImage.FindAffectedArea());
+        savedChunks = new CommittedChunkStorage(layer.LayerImage, layerInfo.Area.Chunks);
         
         layer.LayerImage.CommitChanges();
         return layerInfo;
@@ -40,6 +40,6 @@ internal sealed class ApplyMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
-        return new LayerImageChunks_ChangeInfo(structureMemberGuid, affected);
+        return new LayerImageArea_ChangeInfo(structureMemberGuid, affected);
     }
 }

+ 107 - 23
src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -10,12 +10,26 @@ public static class ChunkRenderer
 {
     private static readonly Paint ClippingPaint = new Paint() { BlendMode = BlendMode.DstIn };
 
-    public static OneOf<Chunk, EmptyChunk> MergeWholeStructure(VecI chunkPos, ChunkResolution resolution, IReadOnlyFolder root)
+    private static RectI? TransfromClipRect(RectI? globalClippingRect, ChunkResolution resolution, VecI chunkPos)
+    {
+        if (globalClippingRect is null)
+            return null;
+        double multiplier = resolution.Multiplier();
+        VecI pixelChunkPos = chunkPos * (int)(ChunkyImage.FullChunkSize * multiplier);
+        return new()
+        {
+            TopLeft = (VecI)(globalClippingRect.Value.TopLeft * multiplier).Floor() - pixelChunkPos,
+            BottomRight = (VecI)(globalClippingRect.Value.BottomRight * multiplier).Floor() - pixelChunkPos
+        };
+    }
+
+    public static OneOf<Chunk, EmptyChunk> MergeWholeStructure(VecI chunkPos, ChunkResolution resolution, IReadOnlyFolder root, RectI? globalClippingRect = null)
     {
         using RenderingContext context = new();
         try
         {
-            return MergeFolderContents(context, chunkPos, resolution, root, new All());
+            RectI? transformedClippingRect = TransfromClipRect(globalClippingRect, resolution, chunkPos);
+            return MergeFolderContents(context, chunkPos, resolution, root, new All(), transformedClippingRect);
         }
         catch (ObjectDisposedException)
         {
@@ -23,12 +37,13 @@ public static class ChunkRenderer
         }
     }
 
-    public static OneOf<Chunk, EmptyChunk> MergeChosenMembers(VecI chunkPos, ChunkResolution resolution, IReadOnlyFolder root, HashSet<Guid> members)
+    public static OneOf<Chunk, EmptyChunk> MergeChosenMembers(VecI chunkPos, ChunkResolution resolution, IReadOnlyFolder root, HashSet<Guid> members, RectI? globalClippingRect = null)
     {
         using RenderingContext context = new();
         try
         {
-            return MergeFolderContents(context, chunkPos, resolution, root, members);
+            RectI? transformedClippingRect = TransfromClipRect(globalClippingRect, resolution, chunkPos);
+            return MergeFolderContents(context, chunkPos, resolution, root, members, transformedClippingRect);
         }
         catch (ObjectDisposedException)
         {
@@ -36,8 +51,14 @@ public static class ChunkRenderer
         }
     }
 
-    private static OneOf<EmptyChunk, Chunk> RenderLayerWithMask
-        (RenderingContext context, Chunk targetChunk, VecI chunkPos, ChunkResolution resolution, IReadOnlyLayer layer, OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk)
+    private static OneOf<EmptyChunk, Chunk> RenderLayerWithMask(
+        RenderingContext context,
+        Chunk targetChunk,
+        VecI chunkPos,
+        ChunkResolution resolution,
+        IReadOnlyLayer layer,
+        OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
+        RectI? transformedClippingRect)
     {
         if (
             clippingChunk.IsT1 ||
@@ -50,6 +71,14 @@ public static class ChunkRenderer
         context.UpdateFromMember(layer);
 
         Chunk renderingResult = Chunk.Create(resolution);
+        if (transformedClippingRect is not null)
+        {
+            renderingResult.Surface.DrawingSurface.Canvas.Save();
+            renderingResult.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+            targetChunk.Surface.DrawingSurface.Canvas.Save();
+            targetChunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+        }
+
         if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.DrawingSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
             renderingResult.Dispose();
@@ -64,23 +93,42 @@ public static class ChunkRenderer
         }
 
         if (clippingChunk.IsT2)
-            OperationHelper.ClampAlpha(renderingResult.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface);
+            OperationHelper.ClampAlpha(renderingResult.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface, transformedClippingRect);
 
         targetChunk.Surface.DrawingSurface.Canvas.DrawSurface(renderingResult.Surface.DrawingSurface, 0, 0, context.BlendModePaint);
+        if (transformedClippingRect is not null)
+        {
+            renderingResult.Surface.DrawingSurface.Canvas.Restore();
+            targetChunk.Surface.DrawingSurface.Canvas.Restore();
+        }
+
         return renderingResult;
     }
 
-    private static OneOf<EmptyChunk, Chunk> RenderLayerSaveResult
-        (RenderingContext context, Chunk targetChunk, VecI chunkPos, ChunkResolution resolution, IReadOnlyLayer layer, OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk)
+    private static OneOf<EmptyChunk, Chunk> RenderLayerSaveResult(
+        RenderingContext context,
+        Chunk targetChunk,
+        VecI chunkPos,
+        ChunkResolution resolution,
+        IReadOnlyLayer layer,
+        OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
+        RectI? transformedClippingRect)
     {
         if (clippingChunk.IsT1 || !layer.IsVisible || layer.Opacity == 0)
             return new EmptyChunk();
 
         if (layer.Mask is not null && layer.MaskIsVisible)
-            return RenderLayerWithMask(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+            return RenderLayerWithMask(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
 
         context.UpdateFromMember(layer);
         Chunk renderingResult = Chunk.Create(resolution);
+        if (transformedClippingRect is not null)
+        {
+            renderingResult.Surface.DrawingSurface.Canvas.Save();
+            renderingResult.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+            targetChunk.Surface.DrawingSurface.Canvas.Save();
+            targetChunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+        }
         if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.DrawingSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
             renderingResult.Dispose();
@@ -88,19 +136,31 @@ public static class ChunkRenderer
         }
 
         if (clippingChunk.IsT2)
-            OperationHelper.ClampAlpha(renderingResult.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface);
+            OperationHelper.ClampAlpha(renderingResult.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface, transformedClippingRect);
         targetChunk.Surface.DrawingSurface.Canvas.DrawSurface(renderingResult.Surface.DrawingSurface, 0, 0, context.BlendModePaint);
+
+        if (transformedClippingRect is not null)
+        {
+            renderingResult.Surface.DrawingSurface.Canvas.Restore();
+            targetChunk.Surface.DrawingSurface.Canvas.Restore();
+        }
         return renderingResult;
     }
 
-    private static void RenderLayer
-        (RenderingContext context, Chunk targetChunk, VecI chunkPos, ChunkResolution resolution, IReadOnlyLayer layer, OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk)
+    private static void RenderLayer(
+        RenderingContext context,
+        Chunk targetChunk,
+        VecI chunkPos,
+        ChunkResolution resolution,
+        IReadOnlyLayer layer,
+        OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
+        RectI? transformedClippingRect)
     {
         if (clippingChunk.IsT1 || !layer.IsVisible || layer.Opacity == 0)
             return;
         if (layer.Mask is not null && layer.MaskIsVisible)
         {
-            var result = RenderLayerWithMask(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+            var result = RenderLayerWithMask(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
             if (result.IsT1)
                 result.AsT1.Dispose();
             return;
@@ -108,13 +168,21 @@ public static class ChunkRenderer
         // clipping chunk requires a temp chunk anyway so we could as well reuse the code from RenderLayerSaveResult
         if (clippingChunk.IsT2)
         {
-            var result = RenderLayerSaveResult(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+            var result = RenderLayerSaveResult(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
             if (result.IsT1)
                 result.AsT1.Dispose();
             return;
         }
         context.UpdateFromMember(layer);
+
+        if (transformedClippingRect is not null)
+        {
+            targetChunk.Surface.DrawingSurface.Canvas.Save();
+            targetChunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+        }
         layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, targetChunk.Surface.DrawingSurface, VecI.Zero, context.BlendModeOpacityPaint);
+        if (transformedClippingRect is not null)
+            targetChunk.Surface.DrawingSurface.Canvas.Restore();
     }
 
     private static OneOf<EmptyChunk, Chunk> RenderFolder(
@@ -124,7 +192,8 @@ public static class ChunkRenderer
         ChunkResolution resolution,
         IReadOnlyFolder folder,
         OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
-        OneOf<All, HashSet<Guid>> membersToMerge)
+        OneOf<All, HashSet<Guid>> membersToMerge,
+        RectI? transformedClippingRect)
     {
         if (
             clippingChunk.IsT1 ||
@@ -135,11 +204,19 @@ public static class ChunkRenderer
         )
             return new EmptyChunk();
 
-        OneOf<Chunk, EmptyChunk> maybeContents = MergeFolderContents(context, chunkPos, resolution, folder, membersToMerge);
+        OneOf<Chunk, EmptyChunk> maybeContents = MergeFolderContents(context, chunkPos, resolution, folder, membersToMerge, transformedClippingRect);
         if (maybeContents.IsT1)
             return new EmptyChunk();
         Chunk contents = maybeContents.AsT0;
 
+        if (transformedClippingRect is not null)
+        {
+            contents.Surface.DrawingSurface.Canvas.Save();
+            contents.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+            targetChunk.Surface.DrawingSurface.Canvas.Save();
+            targetChunk.Surface.DrawingSurface.Canvas.ClipRect((RectD)transformedClippingRect);
+        }
+
         if (folder.Mask is not null && folder.MaskIsVisible)
         {
             if (!folder.Mask.DrawMostUpToDateChunkOn(chunkPos, resolution, contents.Surface.DrawingSurface, VecI.Zero, ClippingPaint))
@@ -151,11 +228,17 @@ public static class ChunkRenderer
         }
 
         if (clippingChunk.IsT2)
-            OperationHelper.ClampAlpha(contents.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface);
+            OperationHelper.ClampAlpha(contents.Surface.DrawingSurface, clippingChunk.AsT2.Surface.DrawingSurface, transformedClippingRect);
         context.UpdateFromMember(folder);
         contents.Surface.DrawingSurface.Canvas.DrawSurface(contents.Surface.DrawingSurface, 0, 0, context.ReplacingPaintWithOpacity);
         targetChunk.Surface.DrawingSurface.Canvas.DrawSurface(contents.Surface.DrawingSurface, 0, 0, context.BlendModePaint);
 
+        if (transformedClippingRect is not null)
+        {
+            contents.Surface.DrawingSurface.Canvas.Restore();
+            targetChunk.Surface.DrawingSurface.Canvas.Restore();
+        }
+
         return contents;
     }
 
@@ -164,7 +247,8 @@ public static class ChunkRenderer
         VecI chunkPos,
         ChunkResolution resolution,
         IReadOnlyFolder folder,
-        OneOf<All, HashSet<Guid>> membersToMerge)
+        OneOf<All, HashSet<Guid>> membersToMerge,
+        RectI? transformedClippingRect)
     {
         if (folder.Children.Count == 0)
             return new EmptyChunk();
@@ -196,12 +280,12 @@ public static class ChunkRenderer
             {
                 if (needToSaveClippingChunk)
                 {
-                    OneOf<EmptyChunk, Chunk> result = RenderLayerSaveResult(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+                    OneOf<EmptyChunk, Chunk> result = RenderLayerSaveResult(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
                     clippingChunk = result.IsT0 ? result.AsT0 : result.AsT1;
                 }
                 else
                 {
-                    RenderLayer(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+                    RenderLayer(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
                 }
                 continue;
             }
@@ -217,12 +301,12 @@ public static class ChunkRenderer
                 OneOf<All, HashSet<Guid>> innerMembersToMerge = shouldRenderAllChildren ? new All() : membersToMerge;
                 if (needToSaveClippingChunk)
                 {
-                    OneOf<EmptyChunk, Chunk> result = RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge);
+                    OneOf<EmptyChunk, Chunk> result = RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge, transformedClippingRect);
                     clippingChunk = result.IsT0 ? result.AsT0 : result.AsT1;
                 }
                 else
                 {
-                    RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge);
+                    RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge, transformedClippingRect);
                 }
             }
         }

+ 9 - 5
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -25,14 +25,16 @@ internal class ActionAccumulator
     private DocumentViewModel document;
     private DocumentInternalParts internals;
 
-    private WriteableBitmapUpdater renderer;
+    private CanvasUpdater canvasUpdater;
+    private MemberPreviewUpdater previewUpdater;
 
     public ActionAccumulator(DocumentViewModel doc, DocumentInternalParts internals)
     {
         this.document = doc;
         this.internals = internals;
 
-        renderer = new(doc, internals);
+        canvasUpdater = new(doc, internals);
+        previewUpdater = new(doc, internals);
     }
 
     public void AddFinishedActions(params IAction[] actions)
@@ -111,9 +113,11 @@ internal class ActionAccumulator
             // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
 
             // update the contents of the bitmaps
-            var affectedChunks = new AffectedChunkGatherer(internals.Tracker, optimizedChanges);
-            var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, undoBoundaryPassed);
-            
+            var affectedAreas = new AffectedAreasGatherer(internals.Tracker, optimizedChanges);
+            List<IRenderInfo> renderResult = new();
+            renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+            renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+
             // lock bitmaps
             foreach (var (_, bitmap) in document.LazyBitmaps)
             {

+ 51 - 36
src/PixiEditor/Models/Rendering/AffectedChunkGatherer.cs → src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -11,15 +11,15 @@ using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.Models.Rendering;
 #nullable enable
-internal class AffectedChunkGatherer
+internal class AffectedAreasGatherer
 {
     private readonly DocumentChangeTracker tracker;
 
-    public HashSet<VecI> MainImageChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> ImagePreviewChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> MaskPreviewChunks { get; private set; } = new();
+    public AffectedArea MainImageArea { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
 
-    public AffectedChunkGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
+    public AffectedAreasGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
     {
         this.tracker = tracker;
         ProcessChanges(changes);
@@ -31,18 +31,18 @@ internal class AffectedChunkGatherer
         {
             switch (change)
             {
-                case MaskChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case MaskArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks, true);
-                    AddToMaskPreview(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area, true);
+                    AddToMaskPreview(info.GuidValue, info.Area);
                     break;
-                case LayerImageChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case LayerImageArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area);
                     break;
                 case CreateStructureMember_ChangeInfo info:
                     AddAllToMainImage(info.GuidValue);
@@ -99,7 +99,7 @@ internal class AffectedChunkGatherer
         if (member is IReadOnlyLayer layer)
         {
             var chunks = layer.LayerImage.FindAllChunks();
-            AddToImagePreviews(memberGuid, chunks, ignoreSelf);
+            AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         else if (member is IReadOnlyFolder folder)
         {
@@ -117,7 +117,7 @@ internal class AffectedChunkGatherer
             var chunks = layer.LayerImage.FindAllChunks();
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
-            AddToMainImage(chunks);
+            AddToMainImage(new AffectedArea(chunks));
         }
         else
         {
@@ -132,7 +132,7 @@ internal class AffectedChunkGatherer
         if (member.Mask is not null)
         {
             var chunks = member.Mask.FindAllChunks();
-            AddToMaskPreview(memberGuid, chunks);
+            AddToMaskPreview(memberGuid, new AffectedArea(chunks));
         }
         if (member is IReadOnlyFolder folder)
         {
@@ -142,12 +142,14 @@ internal class AffectedChunkGatherer
     }
 
 
-    private void AddToMainImage(HashSet<VecI> chunks)
+    private void AddToMainImage(AffectedArea area)
     {
-        MainImageChunks.UnionWith(chunks);
+        var temp = MainImageArea;
+        temp.UnionWith(area);
+        MainImageArea = temp;
     }
 
-    private void AddToImagePreviews(Guid memberGuid, HashSet<VecI> chunks, bool ignoreSelf = false)
+    private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     {
         var path = tracker.Document.FindMemberPath(memberGuid);
         if (path.Count < 2)
@@ -155,25 +157,37 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
             var member = path[i];
-            if (!ImagePreviewChunks.ContainsKey(member.GuidValue))
-                ImagePreviewChunks[member.GuidValue] = new HashSet<VecI>(chunks);
+            if (!ImagePreviewAreas.ContainsKey(member.GuidValue))
+            {
+                ImagePreviewAreas[member.GuidValue] = new AffectedArea(area);
+            }
             else
-                ImagePreviewChunks[member.GuidValue].UnionWith(chunks);
+            {
+                var temp = ImagePreviewAreas[member.GuidValue];
+                temp.UnionWith(area);
+                ImagePreviewAreas[member.GuidValue] = temp;
+            }
         }
     }
 
-    private void AddToMaskPreview(Guid memberGuid, HashSet<VecI> chunks)
+    private void AddToMaskPreview(Guid memberGuid, AffectedArea area)
     {
-        if (!MaskPreviewChunks.ContainsKey(memberGuid))
-            MaskPreviewChunks[memberGuid] = new HashSet<VecI>(chunks);
+        if (!MaskPreviewAreas.ContainsKey(memberGuid))
+        {
+            MaskPreviewAreas[memberGuid] = new AffectedArea(area);
+        }
         else
-            MaskPreviewChunks[memberGuid].UnionWith(chunks);
+        {
+            var temp = MaskPreviewAreas[memberGuid];
+            temp.UnionWith(area);
+            MaskPreviewAreas[memberGuid] = temp;
+        }
     }
 
 
     private void AddWholeCanvasToMainImage()
     {
-        AddAllChunks(MainImageChunks);
+        AddWholeArea(MainImageArea);
     }
 
     private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
@@ -185,17 +199,17 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
             var member = path[i];
-            if (!ImagePreviewChunks.ContainsKey(member.GuidValue))
-                ImagePreviewChunks[member.GuidValue] = new HashSet<VecI>();
-            AddAllChunks(ImagePreviewChunks[member.GuidValue]);
+            if (!ImagePreviewAreas.ContainsKey(member.GuidValue))
+                ImagePreviewAreas[member.GuidValue] = new AffectedArea();
+            AddWholeArea(ImagePreviewAreas[member.GuidValue]);
         }
     }
 
     private void AddWholeCanvasToMaskPreview(Guid memberGuid)
     {
-        if (!MaskPreviewChunks.ContainsKey(memberGuid))
-            MaskPreviewChunks[memberGuid] = new HashSet<VecI>();
-        AddAllChunks(MaskPreviewChunks[memberGuid]);
+        if (!MaskPreviewAreas.ContainsKey(memberGuid))
+            MaskPreviewAreas[memberGuid] = new AffectedArea();
+        AddWholeArea(MaskPreviewAreas[memberGuid]);
     }
 
 
@@ -209,7 +223,7 @@ internal class AffectedChunkGatherer
         tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
     }
 
-    private void AddAllChunks(HashSet<VecI> chunks)
+    private void AddWholeArea(AffectedArea area)
     {
         VecI size = new(
             (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
@@ -218,8 +232,9 @@ internal class AffectedChunkGatherer
         {
             for (int j = 0; j < size.Y; j++)
             {
-                chunks.Add(new(i, j));
+                area.Chunks.Add(new(i, j));
             }
         }
+        area.GlobalArea = new RectI(VecI.Zero, tracker.Document.Size);
     }
 }

+ 194 - 0
src/PixiEditor/Models/Rendering/CanvasUpdater.cs

@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.Printing;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography.Xml;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.Models.Rendering.RenderInfos;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.Rendering;
+#nullable enable
+internal class CanvasUpdater
+{
+    private readonly DocumentViewModel doc;
+    private readonly DocumentInternalParts internals;
+
+    private static readonly Paint ReplacingPaint = new() { BlendMode = BlendMode.Src };
+    private static readonly Paint ClearPaint = new() { BlendMode = BlendMode.Src, Color = PixiEditor.DrawingApi.Core.ColorsImpl.Colors.Transparent };
+
+    /// <summary>
+    /// Affected chunks that have not been rerendered yet.
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> affectedAndNonRerenderedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+    /// <summary>
+    /// Affected chunks that have not been rerendered yet.
+    /// Doesn't include chunks that were affected after the last time rerenderDelayed was true.
+    /// </summary>
+    private readonly Dictionary<ChunkResolution, HashSet<VecI>> nonRerenderedChunksAffectedBeforeLastRerenderDelayed = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
+
+
+    public CanvasUpdater(DocumentViewModel doc, DocumentInternalParts internals)
+    {
+        this.doc = doc;
+        this.internals = internals;
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public async Task<List<IRenderInfo>> UpdateGatheredChunks
+        (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
+    {
+        return await Task.Run(() => Render(chunkGatherer, rerenderDelayed)).ConfigureAwait(true);
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public List<IRenderInfo> UpdateGatheredChunksSync
+        (AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
+    {
+        return Render(chunkGatherer, rerenderDelayed);
+    }
+
+    private Dictionary<ChunkResolution, HashSet<VecI>> FindChunksVisibleOnViewports(bool onDelayed, bool all)
+    {
+        Dictionary<ChunkResolution, HashSet<VecI>> chunks = new() 
+        { 
+            [ChunkResolution.Full] = new(), 
+            [ChunkResolution.Half] = new(), 
+            [ChunkResolution.Quarter] = new(), 
+            [ChunkResolution.Eighth] = new() 
+        };
+        foreach (var (_, viewport) in internals.State.Viewports)
+        {
+            if (onDelayed != viewport.Delayed && !all)
+                continue;
+
+            var viewportChunks = OperationHelper.FindChunksTouchingRectangle(
+                viewport.Center,
+                viewport.Dimensions,
+                -viewport.Angle,
+                ChunkResolution.Full.PixelSize());
+            chunks[viewport.Resolution].UnionWith(viewportChunks);
+        }
+        return chunks;
+    }
+
+    private Dictionary<ChunkResolution, HashSet<VecI>> FindGlobalChunksToRerender(AffectedAreasGatherer areasGatherer, bool renderDelayed)
+    {
+        // find all affected non rerendered chunks
+        var chunksToRerender = new Dictionary<ChunkResolution, HashSet<VecI>>();
+        foreach (var (res, stored) in affectedAndNonRerenderedChunks)
+        {
+            chunksToRerender[res] = new HashSet<VecI>(stored);
+            chunksToRerender[res].UnionWith(areasGatherer.MainImageArea.Chunks);
+        }
+
+        // find all chunks that would need to be rerendered if affected
+        var chunksToMaybeRerender = FindChunksVisibleOnViewports(false, renderDelayed);
+        if (!renderDelayed)
+        {
+            var chunksOnDelayedViewports = FindChunksVisibleOnViewports(true, false);
+            foreach (var (res, stored) in nonRerenderedChunksAffectedBeforeLastRerenderDelayed)
+            {
+                chunksOnDelayedViewports[res].IntersectWith(stored);
+                chunksToMaybeRerender[res].UnionWith(chunksOnDelayedViewports[res]);
+            }
+        }
+
+        // find affected chunks that need to be rerendered right now
+        foreach (var (res, toRerender) in chunksToRerender)
+        {
+            toRerender.IntersectWith(chunksToMaybeRerender[res]);
+        }
+
+        return chunksToRerender;
+    }
+
+    private void UpdateAffectedNonRerenderedChunks(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender)
+    {
+        foreach (var (res, chunks) in chunksToRerender)
+        {
+            affectedAndNonRerenderedChunks[res].ExceptWith(chunks);
+            nonRerenderedChunksAffectedBeforeLastRerenderDelayed[res].ExceptWith(chunks);
+        }
+    }
+
+    private List<IRenderInfo> Render(AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
+    {
+        if (chunkGatherer.MainImageArea.GlobalArea is null)
+            return new();
+
+        Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender = FindGlobalChunksToRerender(chunkGatherer, rerenderDelayed);
+
+        bool updatingStoredChunks = false;
+        foreach (var (res, stored) in affectedAndNonRerenderedChunks)
+        {
+            HashSet<VecI> storedCopy = new HashSet<VecI>(stored);
+            storedCopy.IntersectWith(chunksToRerender[res]);
+            if (storedCopy.Count > 0)
+            {
+                updatingStoredChunks = true;
+                break;
+            }
+        }
+
+        UpdateAffectedNonRerenderedChunks(chunksToRerender);
+
+        List<IRenderInfo> infos = new();
+        UpdateMainImage(chunksToRerender, updatingStoredChunks ? null : chunkGatherer.MainImageArea.GlobalArea.Value, infos);
+        return infos;
+    }
+
+    private void UpdateMainImage(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender, RectI? globalClippingRectangle, List<IRenderInfo> infos)
+    {
+        foreach (var (resolution, chunks) in chunksToRerender)
+        {
+            int chunkSize = resolution.PixelSize();
+            DrawingSurface screenSurface = doc.Surfaces[resolution];
+            foreach (var chunkPos in chunks)
+            {
+                RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle);
+                infos.Add(new DirtyRect_RenderInfo(
+                    chunkPos * chunkSize,
+                    new(chunkSize, chunkSize),
+                    resolution
+                ));
+            }
+        }
+    }
+
+    private void RenderChunk(VecI chunkPos, DrawingSurface screenSurface, ChunkResolution resolution, RectI? globalClippingRectangle)
+    {
+        if (globalClippingRectangle is not null)
+        {
+            screenSurface.Canvas.Save();
+            screenSurface.Canvas.ClipRect((RectD)globalClippingRectangle);
+        }
+
+        ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot, globalClippingRectangle).Switch(
+            (Chunk chunk) =>
+            {
+                screenSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
+                chunk.Dispose();
+            },
+            (EmptyChunk _) =>
+            {
+                var pos = chunkPos * resolution.PixelSize();
+                screenSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
+            });
+
+        if (globalClippingRectangle is not null)
+            screenSurface.Canvas.Restore();
+    }
+}

+ 217 - 0
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Models.DocumentModels;
+using PixiEditor.Models.Rendering.RenderInfos;
+using PixiEditor.ViewModels.SubViewModels.Document;
+
+namespace PixiEditor.Models.Rendering;
+internal class MemberPreviewUpdater
+{
+    private readonly DocumentViewModel doc;
+    private readonly DocumentInternalParts internals;
+
+    private Dictionary<Guid, AffectedArea> previewDelayedAreas = new();
+    private Dictionary<Guid, AffectedArea> maskPreviewDelayedAreas = new();
+
+    private static readonly Paint SmoothReplacingPaint = new() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium, IsAntiAliased = true };
+    private static readonly Paint ClearPaint = new() { BlendMode = BlendMode.Src, Color = PixiEditor.DrawingApi.Core.ColorsImpl.Colors.Transparent };
+
+    public MemberPreviewUpdater(DocumentViewModel doc, DocumentInternalParts internals)
+    {
+        this.doc = doc;
+        this.internals = internals;
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public async Task<List<IRenderInfo>> UpdateGatheredChunks
+        (AffectedAreasGatherer chunkGatherer, bool rerenderPreviews)
+    {
+        return await Task.Run(() => Render(chunkGatherer, rerenderPreviews)).ConfigureAwait(true);
+    }
+
+    /// <summary>
+    /// Don't call this outside ActionAccumulator
+    /// </summary>
+    public List<IRenderInfo> UpdateGatheredChunksSync
+        (AffectedAreasGatherer chunkGatherer, bool rerenderPreviews)
+    {
+        return Render(chunkGatherer, rerenderPreviews);
+    }
+
+    private List<IRenderInfo> Render(AffectedAreasGatherer chunkGatherer, bool rerenderPreviews)
+    {
+        List<IRenderInfo> infos = new();
+
+        var (imagePreviewChunksToRerender, maskPreviewChunksToRerender) = FindPreviewChunksToRerender(chunkGatherer, !rerenderPreviews);
+        var previewSize = StructureMemberViewModel.CalculatePreviewSize(internals.Tracker.Document.Size);
+        float scaling = (float)previewSize.X / doc.SizeBindable.X;
+        UpdateImagePreviews(imagePreviewChunksToRerender, scaling, infos);
+        UpdateMaskPreviews(maskPreviewChunksToRerender, scaling, infos);
+
+        return infos;
+    }
+
+    private static void AddAreas(Dictionary<Guid, AffectedArea> from, Dictionary<Guid, AffectedArea> to)
+    {
+        foreach ((Guid guid, AffectedArea area) in from)
+        {
+            if (!to.ContainsKey(guid))
+                to[guid] = new AffectedArea();
+            var toArea = to[guid];
+            toArea.UnionWith(area);
+            to[guid] = toArea;
+        }
+    }
+
+    private (Dictionary<Guid, AffectedArea> image, Dictionary<Guid, AffectedArea> mask) FindPreviewChunksToRerender
+        (AffectedAreasGatherer areasGatherer, bool delay)
+    {
+        AddAreas(areasGatherer.ImagePreviewAreas, previewDelayedAreas);
+        AddAreas(areasGatherer.MaskPreviewAreas, maskPreviewDelayedAreas);
+        if (delay)
+            return (new(), new());
+        var result = (previewPostponedChunks: previewDelayedAreas, maskPostponedChunks: maskPreviewDelayedAreas);
+        previewDelayedAreas = new();
+        maskPreviewDelayedAreas = new();
+        return result;
+    }
+
+    private void UpdateImagePreviews(Dictionary<Guid, AffectedArea> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        UpdateWholeCanvasPreview(imagePreviewChunks, scaling, infos);
+        UpdateMembersImagePreviews(imagePreviewChunks, scaling, infos);
+    }
+
+    private void UpdateWholeCanvasPreview(Dictionary<Guid, AffectedArea> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        // update preview of the whole canvas
+        var cumulative = imagePreviewChunks.Aggregate(new AffectedArea(), (set, pair) =>
+        {
+            set.UnionWith(pair.Value);
+            return set;
+        });
+        if (cumulative.GlobalArea is null)
+            return;
+
+        bool somethingChanged = false;
+        foreach (var chunkPos in cumulative.Chunks)
+        {
+            somethingChanged = true;
+            ChunkResolution resolution = scaling switch
+            {
+                > 1 / 2f => ChunkResolution.Full,
+                > 1 / 4f => ChunkResolution.Half,
+                > 1 / 8f => ChunkResolution.Quarter,
+                _ => ChunkResolution.Eighth,
+            };
+            var pos = chunkPos * resolution.PixelSize();
+            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot);
+            doc.PreviewSurface.Canvas.Save();
+            doc.PreviewSurface.Canvas.Scale(scaling);
+            doc.PreviewSurface.Canvas.ClipRect((RectD)cumulative.GlobalArea);
+            doc.PreviewSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
+            if (rendered.IsT1)
+            {
+                doc.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
+            }
+            else if (rendered.IsT0)
+            {
+                using var renderedChunk = rendered.AsT0;
+                renderedChunk.DrawOnSurface(doc.PreviewSurface, pos, SmoothReplacingPaint);
+            }
+            doc.PreviewSurface.Canvas.Restore();
+        }
+        if (somethingChanged)
+            infos.Add(new CanvasPreviewDirty_RenderInfo());
+    }
+
+    private void UpdateMembersImagePreviews(Dictionary<Guid, AffectedArea> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        foreach (var (guid, area) in imagePreviewChunks)
+        {
+            if (area.GlobalArea is null)
+                continue;
+            var memberVM = doc.StructureHelper.Find(guid);
+            if (memberVM is null)
+                continue;
+            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
+
+            memberVM.PreviewSurface.Canvas.Save();
+            memberVM.PreviewSurface.Canvas.Scale(scaling);
+            memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+            if (memberVM is LayerViewModel)
+            {
+                var layer = (IReadOnlyLayer)member;
+                foreach (var chunk in area.Chunks)
+                {
+                    var pos = chunk * ChunkResolution.Full.PixelSize();
+                    // the full res chunks are already rendered so drawing them again should be fast
+                    if (!layer.LayerImage.DrawMostUpToDateChunkOn
+                            (chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint))
+                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
+                }
+                infos.Add(new PreviewDirty_RenderInfo(guid));
+            }
+            else if (memberVM is FolderViewModel)
+            {
+                var folder = (IReadOnlyFolder)member;
+                foreach (var chunk in area.Chunks)
+                {
+                    var pos = chunk * ChunkResolution.Full.PixelSize();
+                    // drawing in full res here is kinda slow
+                    // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
+                    OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder);
+                    if (rendered.IsT0)
+                    {
+                        memberVM.PreviewSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos, SmoothReplacingPaint);
+                        rendered.AsT0.Dispose();
+                    }
+                    else
+                    {
+                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint);
+                    }
+                }
+                infos.Add(new PreviewDirty_RenderInfo(guid));
+            }
+            memberVM.PreviewSurface.Canvas.Restore();
+        }
+    }
+
+    private void UpdateMaskPreviews(Dictionary<Guid, AffectedArea> maskPreviewChunks, float scaling, List<IRenderInfo> infos)
+    {
+        foreach (var (guid, area) in maskPreviewChunks)
+        {
+            if (area.GlobalArea is null)
+                continue;
+            var memberVM = doc.StructureHelper.Find(guid);
+            if (memberVM is null || !memberVM.HasMaskBindable)
+                continue;
+
+            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
+            memberVM.MaskPreviewSurface!.Canvas.Save();
+            memberVM.MaskPreviewSurface.Canvas.Scale(scaling);
+            memberVM.MaskPreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+            foreach (var chunk in area.Chunks)
+            {
+                var pos = chunk * ChunkResolution.Full.PixelSize();
+                member.Mask!.DrawMostUpToDateChunkOn
+                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface, pos, SmoothReplacingPaint);
+            }
+
+            memberVM.MaskPreviewSurface.Canvas.Restore();
+            infos.Add(new MaskPreviewDirty_RenderInfo(guid));
+        }
+    }
+}

+ 0 - 310
src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs

@@ -1,310 +0,0 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using ChunkyImageLib.Operations;
-using PixiEditor.ChangeableDocument.Changeables.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core.Numerics;
-using PixiEditor.DrawingApi.Core.Surface;
-using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
-using PixiEditor.Models.DocumentModels;
-using PixiEditor.Models.Rendering.RenderInfos;
-using PixiEditor.ViewModels.SubViewModels.Document;
-
-namespace PixiEditor.Models.Rendering;
-#nullable enable
-internal class WriteableBitmapUpdater
-{
-    private readonly DocumentViewModel doc;
-    private readonly DocumentInternalParts internals;
-
-    private static readonly Paint ReplacingPaint = new() { BlendMode = BlendMode.Src };
-    private static readonly Paint SmoothReplacingPaint = new() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.Medium, IsAntiAliased = true };
-    private static readonly Paint ClearPaint = new() { BlendMode = BlendMode.Src, Color = PixiEditor.DrawingApi.Core.ColorsImpl.Colors.Transparent };
-
-    /// <summary>
-    /// Chunks that have been updated but don't need to be re-rendered because they are out of view
-    /// </summary>
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
-
-    /// <summary>
-    /// The state of globalPostponedChunks during the last update of global delayed chunks (when you finish using a tool)
-    /// It is required in case the viewport is moved while you are using a tool. In this case the newly visible chunks on delayed viewports
-    /// need to be re-rendered, even though normally re-render only happens after you're done with some tool.
-    /// Because the viewport still has the old version of the image there is no point in re-rendering everything from globalPostponedChunks.
-    /// It's enough to re-render the chunks that were postponed back when the delayed viewports were last updated fully.
-    /// </summary>
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalPostponedForDelayed = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
-
-    /// <summary>
-    /// Chunks that have been updated but don't need to be re-rendered because all viewports that see them have Delayed == true
-    /// These chunks are updated after you finish using a tool
-    /// </summary>
-    private readonly Dictionary<ChunkResolution, HashSet<VecI>> globalDelayedChunks = new() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
-
-    private Dictionary<Guid, HashSet<VecI>> previewDelayedChunks = new();
-    private Dictionary<Guid, HashSet<VecI>> maskPreviewDelayedChunks = new();
-
-    public WriteableBitmapUpdater(DocumentViewModel doc, DocumentInternalParts internals)
-    {
-        this.doc = doc;
-        this.internals = internals;
-    }
-
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public async Task<List<IRenderInfo>> UpdateGatheredChunks
-        (AffectedChunkGatherer chunkGatherer, bool updateDelayed)
-    {
-        return await Task.Run(() => Render(chunkGatherer, updateDelayed)).ConfigureAwait(true);
-    }
-
-    /// <summary>
-    /// Don't call this outside ActionAccumulator
-    /// </summary>
-    public List<IRenderInfo> UpdateGatheredChunksSync
-        (AffectedChunkGatherer chunkGatherer, bool updateDelayed)
-    {
-        return Render(chunkGatherer, updateDelayed);
-    }
-
-    private Dictionary<ChunkResolution, HashSet<VecI>> FindGlobalChunksToRerender(AffectedChunkGatherer chunkGatherer, bool renderDelayed)
-    {
-        // add all affected chunks to postponed
-        foreach (var (_, postponed) in globalPostponedChunks)
-        {
-            postponed.UnionWith(chunkGatherer.MainImageChunks);
-        }
-
-        // find all chunks that are on viewports and on delayed viewports
-        var chunksToUpdate = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
-
-        var chunksOnDelayedViewports = new Dictionary<ChunkResolution, HashSet<VecI>>() { [ChunkResolution.Full] = new(), [ChunkResolution.Half] = new(), [ChunkResolution.Quarter] = new(), [ChunkResolution.Eighth] = new() };
-
-        foreach (var (_, viewport) in internals.State.Viewports)
-        {
-            var viewportChunks = OperationHelper.FindChunksTouchingRectangle(
-                viewport.Center,
-                viewport.Dimensions,
-                -viewport.Angle,
-                ChunkResolution.Full.PixelSize());
-            if (viewport.Delayed)
-                chunksOnDelayedViewports[viewport.Resolution].UnionWith(viewportChunks);
-            else
-                chunksToUpdate[viewport.Resolution].UnionWith(viewportChunks);
-        }
-
-        // exclude the chunks that don't need to be updated, remove chunks that will be updated from postponed
-        foreach (var (res, postponed) in globalPostponedChunks)
-        {
-            chunksToUpdate[res].IntersectWith(postponed);
-            chunksOnDelayedViewports[res].IntersectWith(postponed);
-            postponed.ExceptWith(chunksToUpdate[res]);
-        }
-
-        // decide what to do about the delayed chunks
-        if (renderDelayed)
-        {
-            foreach (var (res, postponed) in globalPostponedChunks)
-            {
-                chunksToUpdate[res].UnionWith(chunksOnDelayedViewports[res]);
-                postponed.ExceptWith(chunksOnDelayedViewports[res]);
-                globalPostponedForDelayed[res] = new HashSet<VecI>(postponed);
-            }
-        }
-        else
-        {
-            foreach (var (res, postponed) in globalPostponedChunks)
-            {
-                chunksOnDelayedViewports[res].IntersectWith(globalPostponedForDelayed[res]);
-                globalPostponedForDelayed[res].ExceptWith(chunksOnDelayedViewports[res]);
-
-                chunksToUpdate[res].UnionWith(chunksOnDelayedViewports[res]);
-                postponed.ExceptWith(chunksOnDelayedViewports[res]);
-            }
-        }
-
-        return chunksToUpdate;
-    }
-
-
-    private static void AddChunks(Dictionary<Guid, HashSet<VecI>> from, Dictionary<Guid, HashSet<VecI>> to)
-    {
-        foreach ((Guid guid, HashSet<VecI> chunks) in from)
-        {
-            if (!to.ContainsKey(guid))
-                to[guid] = new HashSet<VecI>();
-            to[guid].UnionWith(chunks);
-        }
-    }
-
-    private (Dictionary<Guid, HashSet<VecI>> image, Dictionary<Guid, HashSet<VecI>> mask) FindPreviewChunksToRerender
-        (AffectedChunkGatherer chunkGatherer, bool postpone)
-    {
-        AddChunks(chunkGatherer.ImagePreviewChunks, previewDelayedChunks);
-        AddChunks(chunkGatherer.MaskPreviewChunks, maskPreviewDelayedChunks);
-        if (postpone)
-            return (new(), new());
-        var result = (previewPostponedChunks: previewDelayedChunks, maskPostponedChunks: maskPreviewDelayedChunks);
-        previewDelayedChunks = new();
-        maskPreviewDelayedChunks = new();
-        return result;
-    }
-
-    private List<IRenderInfo> Render(AffectedChunkGatherer chunkGatherer, bool updateDelayed)
-    {
-        Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender = FindGlobalChunksToRerender(chunkGatherer, updateDelayed);
-
-        List<IRenderInfo> infos = new();
-        UpdateMainImage(chunksToRerender, infos);
-
-        var (imagePreviewChunksToRerender, maskPreviewChunksToRerender) = FindPreviewChunksToRerender(chunkGatherer, !updateDelayed);
-        var previewSize = StructureMemberViewModel.CalculatePreviewSize(internals.Tracker.Document.Size);
-        float scaling = (float)previewSize.X / doc.SizeBindable.X;
-        UpdateImagePreviews(imagePreviewChunksToRerender, scaling, infos);
-        UpdateMaskPreviews(maskPreviewChunksToRerender, scaling, infos);
-
-        return infos;
-    }
-
-    private void UpdateImagePreviews(Dictionary<Guid, HashSet<VecI>> imagePreviewChunks, float scaling, List<IRenderInfo> infos)
-    {
-        // update preview of the whole canvas
-        var cumulative = imagePreviewChunks.Aggregate(new HashSet<VecI>(), (set, pair) =>
-        {
-            set.UnionWith(pair.Value);
-            return set;
-        });
-        bool somethingChanged = false;
-        foreach (var chunkPos in cumulative)
-        {
-            somethingChanged = true;
-            ChunkResolution resolution = scaling switch
-            {
-                > 1 / 2f => ChunkResolution.Full,
-                > 1 / 4f => ChunkResolution.Half,
-                > 1 / 8f => ChunkResolution.Quarter,
-                _ => ChunkResolution.Eighth,
-            };
-            var pos = chunkPos * resolution.PixelSize();
-            var rendered = ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot);
-            doc.PreviewSurface.Canvas.Save();
-            doc.PreviewSurface.Canvas.Scale(scaling);
-            doc.PreviewSurface.Canvas.Scale(1 / (float)resolution.Multiplier());
-            if (rendered.IsT1)
-            {
-                doc.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
-                return;
-            }
-            using var renderedChunk = rendered.AsT0;
-            renderedChunk.DrawOnSurface(doc.PreviewSurface, pos, SmoothReplacingPaint);
-            doc.PreviewSurface.Canvas.Restore();
-        }
-        if (somethingChanged)
-            infos.Add(new CanvasPreviewDirty_RenderInfo());
-
-        // update previews of individual members
-        foreach (var (guid, chunks) in imagePreviewChunks)
-        {
-            var memberVM = doc.StructureHelper.Find(guid);
-            if (memberVM is null)
-                continue;
-            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
-
-            memberVM.PreviewSurface.Canvas.Save();
-            memberVM.PreviewSurface.Canvas.Scale(scaling);
-            if (memberVM is LayerViewModel)
-            {
-                var layer = (IReadOnlyLayer)member;
-                foreach (var chunk in chunks)
-                {
-                    var pos = chunk * ChunkResolution.Full.PixelSize();
-                    // the full res chunks are already rendered so drawing them again should be fast
-                    if (!layer.LayerImage.DrawMostUpToDateChunkOn
-                            (chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, SmoothReplacingPaint))
-                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
-                }
-                infos.Add(new PreviewDirty_RenderInfo(guid));
-            }
-            else if (memberVM is FolderViewModel)
-            {
-                var folder = (IReadOnlyFolder)member;
-                foreach (var chunk in chunks)
-                {
-                    var pos = chunk * ChunkResolution.Full.PixelSize();
-                    // drawing in full res here is kinda slow
-                    // we could switch to a lower resolution based on (canvas size / preview size) to make it run faster
-                    OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder);
-                    if (rendered.IsT0)
-                    {
-                        memberVM.PreviewSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos, SmoothReplacingPaint);
-                        rendered.AsT0.Dispose();
-                    }
-                    else
-                    {
-                        memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint);
-                    }
-                }
-                infos.Add(new PreviewDirty_RenderInfo(guid));
-            }
-            memberVM.PreviewSurface.Canvas.Restore();
-        }
-    }
-
-    private void UpdateMaskPreviews(Dictionary<Guid, HashSet<VecI>> maskPreviewChunks, float scaling, List<IRenderInfo> infos)
-    {
-        foreach (var (guid, chunks) in maskPreviewChunks)
-        {
-            var memberVM = doc.StructureHelper.Find(guid);
-            if (memberVM is null || !memberVM.HasMaskBindable)
-                continue;
-
-            var member = internals.Tracker.Document.FindMemberOrThrow(guid);
-            memberVM.MaskPreviewSurface!.Canvas.Save();
-            memberVM.MaskPreviewSurface.Canvas.Scale(scaling);
-
-            foreach (var chunk in chunks)
-            {
-                var pos = chunk * ChunkResolution.Full.PixelSize();
-                member.Mask!.DrawMostUpToDateChunkOn
-                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface, pos, SmoothReplacingPaint);
-            }
-
-            memberVM.MaskPreviewSurface.Canvas.Restore();
-            infos.Add(new MaskPreviewDirty_RenderInfo(guid));
-        }
-    }
-
-    private void UpdateMainImage(Dictionary<ChunkResolution, HashSet<VecI>> chunksToRerender, List<IRenderInfo> infos)
-    {
-        foreach (var (resolution, chunks) in chunksToRerender)
-        {
-            int chunkSize = resolution.PixelSize();
-            DrawingSurface screenSurface = doc.Surfaces[resolution];
-            foreach (var chunkPos in chunks)
-            {
-                RenderChunk(chunkPos, screenSurface, resolution);
-                infos.Add(new DirtyRect_RenderInfo(
-                    chunkPos * chunkSize,
-                    new(chunkSize, chunkSize),
-                    resolution
-                ));
-            }
-        }
-    }
-
-    private void RenderChunk(VecI chunkPos, DrawingSurface screenSurface, ChunkResolution resolution)
-    {
-        ChunkRenderer.MergeWholeStructure(chunkPos, resolution, internals.Tracker.Document.StructureRoot).Switch(
-            (Chunk chunk) =>
-            {
-                screenSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, chunkPos.Multiply(chunk.PixelSize), ReplacingPaint);
-                chunk.Dispose();
-            },
-            (EmptyChunk _) =>
-            {
-                var pos = chunkPos * resolution.PixelSize();
-                screenSurface.Canvas.DrawRect(pos.X, pos.Y, resolution.PixelSize(), resolution.PixelSize(), ClearPaint);
-            });
-    }
-}

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -401,7 +401,7 @@ internal partial class DocumentViewModel : NotifyableObject
             if (scope == DocumentScope.AllLayers)
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot)
+                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot, new RectI(pos, VecI.One))
                     .Match<Color>(
                         (Chunk chunk) =>
                         {