Browse Source

Merge pull request #444 from PixiEditor/better-dirty-rectangles

Faster dirty rectangles, don't update whole chunks, update just the changed area
Egor Mozgovoy 2 years ago
parent
commit
557c685875
58 changed files with 1128 additions and 617 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. 104 23
      src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs
  51. 33 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs
  52. 45 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs
  53. 9 16
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  54. 54 37
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  55. 215 0
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  56. 217 0
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  57. 0 310
      src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs
  58. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

+ 1 - 0
src/ChunkyImageLib/Chunk.cs

@@ -94,6 +94,7 @@ public class Chunk : IDisposable
             return;
             return;
         returned = true;
         returned = true;
         Interlocked.Decrement(ref chunkCounter);
         Interlocked.Decrement(ref chunkCounter);
+        Surface.DrawingSurface.Canvas.RestoreToCount(-1);
         ChunkPool.Instance.Push(this);
         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.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using OneOf;
 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 readonly List<ChunkyImage> activeClips = new();
     private BlendMode blendMode = BlendMode.Src;
     private BlendMode blendMode = BlendMode.Src;
     private bool lockTransparency = false;
     private bool lockTransparency = false;
@@ -321,7 +322,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 return true;
                 return true;
             foreach (var operation in queuedOperations)
             foreach (var operation in queuedOperations)
             {
             {
-                if (operation.affectedChunks.Contains(chunkPos))
+                if (operation.affectedArea.Chunks.Contains(chunkPos))
                     return true;
                     return true;
             }
             }
 
 
@@ -645,7 +646,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
             ClearOperation operation = new();
             ClearOperation operation = new();
-            EnqueueOperation(operation, FindAllChunks());
+            EnqueueOperation(operation, new(FindAllChunks()));
         }
         }
     }
     }
 
 
@@ -657,7 +658,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             ThrowIfDisposed();
             ThrowIfDisposed();
             ResizeOperation operation = new(newSize);
             ResizeOperation operation = new(newSize);
             LatestSize = 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)
         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)
             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>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
@@ -733,9 +735,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         lock (lockObject)
         lock (lockObject)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
-            var affectedChunks = FindAffectedChunks();
+            var affectedArea = FindAffectedArea();
 
 
-            foreach (var chunk in affectedChunks)
+            foreach (var chunk in affectedArea.Chunks)
             {
             {
                 MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
                 MaybeCreateAndProcessQueueForChunk(chunk, ChunkResolution.Full);
             }
             }
@@ -876,9 +878,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
             var allChunks = committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
             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;
             return allChunks;
@@ -899,19 +901,25 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// Chunks affected by operations that haven't been committed yet
     /// Chunks affected by operations that haven't been committed yet
     /// </returns>
     /// </returns>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0)
+    public AffectedArea FindAffectedArea(int fromOperationIndex = 0)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
             ThrowIfDisposed();
             ThrowIfDisposed();
             var chunks = new HashSet<VecI>();
             var chunks = new HashSet<VecI>();
+            RectI? rect = null;
+            
             for (int i = fromOperationIndex; i < queuedOperations.Count; i++)
             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++)
         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;
                 continue;
 
 
             if (!initialized)
             if (!initialized)
@@ -944,7 +952,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             }
             }
 
 
             if (chunkData.QueueProgress <= i)
             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)
         if (initialized)
@@ -999,6 +1007,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// </returns>
     /// </returns>
     private bool ApplyOperationToChunk(
     private bool ApplyOperationToChunk(
         IOperation operation,
         IOperation operation,
+        AffectedArea operationAffectedArea,
         OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
         OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
         Chunk targetChunk,
         Chunk targetChunk,
         VecI chunkPos,
         VecI chunkPos,
@@ -1010,16 +1019,16 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
 
         if (operation is IDrawOperation chunkOperation)
         if (operation is IDrawOperation chunkOperation)
         {
         {
-            if (combinedRasterClips.IsT1) //Nothing is visible
+            if (combinedRasterClips.IsT1) // Nothing is visible
                 return chunkData.IsDeleted;
                 return chunkData.IsDeleted;
 
 
             if (chunkData.IsDeleted)
             if (chunkData.IsDeleted)
                 targetChunk.Surface.DrawingSurface.Canvas.Clear();
                 targetChunk.Surface.DrawingSurface.Canvas.Clear();
 
 
             // just regular drawing
             // 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;
                 return false;
             }
             }
 
 
@@ -1029,7 +1038,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
             targetChunk.DrawOnSurface(tempChunk.Surface.DrawingSurface, VecI.Zero, ReplacingPaint);
             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(tempChunk.Surface.DrawingSurface, VecI.Zero, ClippingPaint);
             clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
             clip.DrawOnSurface(targetChunk.Surface.DrawingSurface, VecI.Zero, InverseClippingPaint);
@@ -1046,24 +1055,31 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         return chunkData.IsDeleted;
         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)
         if (clippingPath is not null && !clippingPath.IsEmpty)
         {
         {
-            int count = targetChunk.Surface.DrawingSurface.Canvas.Save();
-
             using VectorPath transformedPath = new(clippingPath);
             using VectorPath transformedPath = new(clippingPath);
-            float scale = (float)resolution.Multiplier();
             VecD trans = -chunkPos * FullChunkSize * scale;
             VecD trans = -chunkPos * FullChunkSize * scale;
+            
             transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
             transformedPath.Transform(Matrix3X3.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
             targetChunk.Surface.DrawingSurface.Canvas.ClipPath(transformedPath);
             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>
     /// <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 * ChunkyImage.FullChunkSize, 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;
                 (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)
     public bool IsPointInside(VecD point)
     {
     {
         var top = TopLeft - TopRight;
         var top = TopLeft - TopRight;
@@ -129,4 +141,12 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopRight = TopRight.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 GetCommittedPixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
-    HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0);
+    AffectedArea FindAffectedArea(int fromOperationIndex = 0);
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindAllChunks();
     HashSet<VecI> FindAllChunks();
 }
 }

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

@@ -16,10 +16,10 @@ internal class ApplyMaskOperation : IDrawOperation
     {
     {
         mask = maskToApply;
         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)
     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();
         surf.Canvas.Restore();
     }
     }
 
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
     {
         RectI bounds = RectI.FromTwoPixels(from, to);
         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)
     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();
         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()
     private VecI GetTopLeft()

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

@@ -29,9 +29,9 @@ internal class ClearPathOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
         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()
     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;
 namespace ChunkyImageLib.Operations;
 
 
@@ -24,9 +25,9 @@ internal class ClearRegionOperation : IMirroredDrawOperation
         chunk.Surface.DrawingSurface.Canvas.Restore();
         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() { }
     public void Dispose() { }
 
 

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

@@ -38,10 +38,10 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
         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));
         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)
     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();
         surf.Canvas.Restore();
     }
     }
 
 
-    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    public AffectedArea FindAffectedArea(VecI imageSize)
     {
     {
         var chunks = OperationHelper.FindChunksTouchingEllipse
         var chunks = OperationHelper.FindChunksTouchingEllipse
             (location.Center, location.Width / 2.0, location.Height / 2.0, ChunkyImage.FullChunkSize);
             (location.Center, location.Width / 2.0, location.Height / 2.0, ChunkyImage.FullChunkSize);
@@ -102,7 +102,7 @@ internal class EllipseOperation : IMirroredDrawOperation
             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
                 (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize));
                 (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)
     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; }
     bool IgnoreEmptyChunks { get; }
     void DrawOnChunk(Chunk chunk, VecI chunkPos);
     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();
         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()
     public void Dispose()
@@ -109,14 +109,20 @@ internal class ImageOperation : IMirroredDrawOperation
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
     public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
     {
     {
         if (verAxisX is not null && horAxisY is not null)
         if (verAxisX is not null && horAxisY is not null)
+        {
             return new ImageOperation
             return new ImageOperation
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+        }
         if (verAxisX is not null)
         if (verAxisX is not null)
+        {
             return new ImageOperation
             return new ImageOperation
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
                 (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
+        }
         if (horAxisY is not null)
         if (horAxisY is not null)
+        {
             return new ImageOperation
             return new ImageOperation
                 (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
                 (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+        }
         return new ImageOperation(corners, 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>
     /// <summary>
     /// toModify[x,y].Alpha = Math.Min(toModify[x,y].Alpha, toGetAlphaFrom[x,y].Alpha)
     /// toModify[x,y].Alpha = Math.Min(toModify[x,y].Alpha, toGetAlphaFrom[x,y].Alpha)
     /// </summary>
     /// </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 map = toModify.PeekPixels();
         using Pixmap refMap = toGetAlphaFrom.PeekPixels();
         using Pixmap refMap = toGetAlphaFrom.PeekPixels();
         long* pixels = (long*)map.GetPixels();
         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)
     public static ShapeCorners ConvertForResolution(ShapeCorners corners, ChunkResolution resolution)
     {
     {
         return new ShapeCorners()
         return new ShapeCorners()

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

@@ -35,9 +35,9 @@ internal class PathOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
         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)
     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();
         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)
     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.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
@@ -35,9 +36,20 @@ internal class PixelsOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
         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)
     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();
         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))
         if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 || (Data.StrokeColor.A == 0 && Data.FillColor.A == 0))
             return new();
             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)
         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);
         var chunks = OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize);
         chunks.ExceptWith(
         chunks.ExceptWith(
@@ -64,7 +67,7 @@ internal class RectangleOperation : IMirroredDrawOperation
                 Data.Size.Abs() - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
                 Data.Size.Abs() - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
                 Data.Angle,
                 Data.Angle,
                 ChunkPool.FullChunkSize));
                 ChunkPool.FullChunkSize));
-        return chunks;
+        return new (chunks, affRect);
     }
     }
 
 
     public void Dispose() { }
     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()
     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)));
         ClearRegionOperation operation = new(new(new(chunkSize, chunkSize), new(chunkSize, chunkSize)));
         var expected = new HashSet<VecI>() { new(1, 1) };
         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);
         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, -0), new(-1, -0), new(0, -0), new(1, -0),
             new(-2,  1), new(-1,  1), new(0,  1), new(1,  1),
             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);
         Assert.Equal(expected, actual);
     }
     }
 #pragma warning restore format
 #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 Surface testImage = new Surface((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         using ImageOperation operation = new((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), testImage);
         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);
         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
 // to keep expected rectangles aligned
 #pragma warning disable format
 #pragma warning disable format
     [Fact]
     [Fact]
-    public void FindAffectedChunks_SmallStrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_SmallStrokeOnly_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (chunkSize / 2, chunkSize / 2, chunkSize, chunkSize);
         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));
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
 
         Assert.Equal(expected, actual);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [Fact]
-    public void FindAffectedChunks_2by2StrokeOnly_FindsCorrectChunks()
+    public void FindAffectedArea_2by2StrokeOnly_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (0, 0, chunkSize * 2, chunkSize * 2);
         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));
         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) };
         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);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [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);
         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));
         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, 2),            new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
             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);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [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);
         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));
         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, -3),              new(-2, -3),
             new(-4, -2), new(-3, -2), new(-2, -2),
             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);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [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);
         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));
         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, 2), new(2, 2), new(3, 2),
             new(1, 3), new(2, 3), new(3, 3),
             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);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [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);
         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));
         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, 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),
             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);
         Assert.Equal(expected, actual);
     }
     }
 
 
     [Fact]
     [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);
         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));
         RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, Colors.Black, Colors.White));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
-        var actual = operation.FindAffectedChunks(new(chunkSize));
+        var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
 
 
         Assert.Equal(expected, actual);
         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;
 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;
 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>
         return new List<IChangeInfo>
         {
         {
             new StructureMemberMask_ChangeInfo(layerGuid, false),
             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);
         ChunkyImage newMask = new ChunkyImage(target.Size);
         savedMask.ApplyChunksToImage(newMask);
         savedMask.ApplyChunksToImage(newMask);
-        var affectedChunksMask = newMask.FindAffectedChunks();
+        var affectedChunksMask = newMask.FindAffectedArea();
         newMask.CommitChanges();
         newMask.CommitChanges();
         layer.Mask = newMask;
         layer.Mask = newMask;
 
 
         savedLayer.ApplyChunksToImage(layer.LayerImage);
         savedLayer.ApplyChunksToImage(layer.LayerImage);
-        var affectedChunksLayer = layer.LayerImage.FindAffectedChunks();
+        var affectedChunksLayer = layer.LayerImage.FindAffectedArea();
         layer.LayerImage.CommitChanges();
         layer.LayerImage.CommitChanges();
 
 
         return new List<IChangeInfo>
         return new List<IChangeInfo>
         {
         {
             new StructureMemberMask_ChangeInfo(layerGuid, true),
             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);
         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(
     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();
         layer.LayerImage.CommitChanges();
         if (firstApply)
         if (firstApply)
             return new None();
             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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, layerGuid, false, ref savedChunks);
         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()
     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();
         RectI intBounds = (RectI)bounds.Intersect(new RectD(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
 
 
         image.EnqueueClearPath(target.Selection.SelectionPath, intBounds);
         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();
         image.CommitChanges();
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     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()
     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();
                 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();
         toDrawOn.LayerImage.CommitChanges();
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return new LayerImageChunks_ChangeInfo(targetLayer, affectedChunks);
+        return new LayerImageArea_ChangeInfo(targetLayer, affArea);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var toDrawOn = target.FindMemberOrThrow<Layer>(targetLayer);
         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()
     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);
         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();
         targetImage.CancelChanges();
 
 
@@ -47,10 +47,10 @@ internal class DrawEllipse_UpdateableChange : UpdateableChange
             targetImage.EnqueueDrawEllipse(location, strokeColor, fillColor, strokeWidth);
             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)
     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 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();
         image.CommitChanges();
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         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)
     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;
         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);
         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 image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        var oldAffected = image.FindAffectedChunks();
+        var oldAffected = image.FindAffectedArea();
         image.CancelChanges();
         image.CancelChanges();
         if (from != to)
         if (from != to)
         {
         {
@@ -56,14 +56,14 @@ internal class DrawLine_UpdateableChange : UpdateableChange
             else
             else
                 image.EnqueueDrawSkiaLine(from, to, caps, strokeWidth, color, BlendMode.SrcOver);
                 image.EnqueueDrawSkiaLine(from, to, caps, strokeWidth, color, BlendMode.SrcOver);
         }
         }
-        var totalAffected = image.FindAffectedChunks();
+        var totalAffected = image.FindAffectedArea();
         totalAffected.UnionWith(oldAffected);
         totalAffected.UnionWith(oldAffected);
         return totalAffected;
         return totalAffected;
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     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)
     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);
         var affected = CommonApply(target);
         if (savedChunks is not null)
         if (savedChunks is not null)
             throw new InvalidOperationException("Trying to save chunks while there are saved chunks already");
             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();
         image.CommitChanges();
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull
             (target, memberGuid, drawOnMask, ref savedChunks);
             (target, memberGuid, drawOnMask, ref savedChunks);
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affected, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
     }
 
 
     public override void Dispose()
     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;
         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();
         targetImage.CancelChanges();
 
 
@@ -40,17 +40,17 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
             targetImage.EnqueueDrawRectangle(rect);
             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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         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)
     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);
         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();
         targetImage.CommitChanges();
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, area, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     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()
     public override void Dispose()

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

@@ -3,22 +3,22 @@
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal static class DrawingChangeHelper
 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);
         var image = GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         return ApplyStoredChunksDisposeAndSetToNull(image, ref storage);
         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)
         if (storage is null)
             throw new InvalidOperationException("No stored chunks to apply");
             throw new InvalidOperationException("No stored chunks to apply");
         storage.ApplyChunksToImage(image);
         storage.ApplyChunksToImage(image);
-        var chunks = image.FindAffectedChunks();
+        var area = image.FindAffectedArea();
         image.CommitChanges();
         image.CommitChanges();
         storage.Dispose();
         storage.Dispose();
         storage = null;
         storage = null;
-        return chunks;
+        return area;
     }
     }
 
 
     public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask)
     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
         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);
             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();
         image.CommitChanges();
         foreach (var chunk in floodFilledChunks.Values)
         foreach (var chunk in floodFilledChunks.Values)
             chunk.Dispose();
             chunk.Dispose();
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     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()
     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.EnqueueDrawEllipse(rect, color, color, 1, srcPaint);
             image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
             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)
     private void FastforwardEnqueueDrawLines(ChunkyImage targetImage)
@@ -112,11 +112,11 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         ignoreInUndo = false;
         ignoreInUndo = false;
         if (firstApply)
         if (firstApply)
         {
         {
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
             image.CommitChanges();
 
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
         }
         else
         else
         {
         {
@@ -125,18 +125,18 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
 
             FastforwardEnqueueDrawLines(image);
             FastforwardEnqueueDrawLines(image);
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
             image.CommitChanges();
 
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
         }
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
         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()
     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;
         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();
         targetImage.CancelChanges();
         if (!ignoreClipsSymmetriesEtc)
         if (!ignoreClipsSymmetriesEtc)
@@ -46,9 +46,9 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         targetImage.EnqueueDrawImage(corners, imageToPaste, RegularPaint, false);
         targetImage.EnqueueDrawImage(corners, imageToPaste, RegularPaint, false);
         hasEnqueudImage = true;
         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)
     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);
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var chunks = DrawImage(target, targetImage);
         var chunks = DrawImage(target, targetImage);
         savedChunks?.Dispose();
         savedChunks?.Dispose();
-        savedChunks = new(targetImage, targetImage.FindAffectedChunks());
+        savedChunks = new(targetImage, targetImage.FindAffectedArea().Chunks);
         targetImage.CommitChanges();
         targetImage.CommitChanges();
         hasEnqueudImage = false;
         hasEnqueudImage = false;
         ignoreInUndo = 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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref savedChunks);
         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()
     public override void Dispose()

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

@@ -111,11 +111,11 @@ internal class PathBasedPen_UpdateableChange : UpdateableChange
             UpdateTempPathFinish();
             UpdateTempPathFinish();
 
 
             image.EnqueueDrawPath(tempPath, color, strokeWidth, StrokeCap.Round, BlendMode.Src);
             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();
             image.CommitChanges();
 
 
-            return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+            return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
         }
         }
         else
         else
         {
         {
@@ -123,11 +123,11 @@ internal class PathBasedPen_UpdateableChange : UpdateableChange
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
             DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
 
 
             FastforwardEnqueueDrawPath(image);
             FastforwardEnqueueDrawPath(image);
-            var affChunks = image.FindAffectedChunks();
-            storedChunks = new CommittedChunkStorage(image, affChunks);
+            var affArea = image.FindAffectedArea();
+            storedChunks = new CommittedChunkStorage(image, affArea.Chunks);
             image.CommitChanges();
             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;
         int opCount = image.QueueLength;
         image.EnqueueDrawPath(tempPath, color, strokeWidth, StrokeCap.Round, BlendMode.Src);
         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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref storedChunks);
         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()
     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;
         int changeCount = image.QueueLength;
         DoDrawingIteration(image, incomingPoints!.Count);
         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)
     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);
             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();
         image.CommitChanges();
-        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affChunks, drawOnMask);
+        return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affArea, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, ref chunkStorage);
         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)
             if (member is not Layer layer)
                 return;
                 return;
             layer.LayerImage.EnqueueReplaceColor(oldColor, newColor);
             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;
             savedChunks[layer.GuidValue] = storage;
             layer.LayerImage.CommitChanges();
             layer.LayerImage.CommitChanges();
-            infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks));
+            infos.Add(new LayerImageArea_ChangeInfo(layer.GuidValue, affArea));
         });
         });
         ignoreInUndo = !savedChunks.Any();
         ignoreInUndo = !savedChunks.Any();
         return infos;
         return infos;
@@ -52,8 +52,8 @@ internal class ReplaceColor_Change : Change
             if (member is not Layer layer)
             if (member is not Layer layer)
                 return;
                 return;
             CommittedChunkStorage? storage = savedChunks[member.GuidValue];
             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;
         savedChunks = null;
         return infos;
         return infos;

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

@@ -4,16 +4,17 @@ namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
 internal static class ShiftLayerHelper
 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 targetImage = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
-        var prevChunks = targetImage.FindAffectedChunks();
+        var prevArea = targetImage.FindAffectedArea();
         targetImage.CancelChanges();
         targetImage.CancelChanges();
         if (!keepOriginal)
         if (!keepOriginal)
             targetImage.EnqueueClear();
             targetImage.EnqueueClear();
         targetImage.EnqueueDrawChunkyImage(delta, targetImage, false, false);
         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>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         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;
             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();
             image.CommitChanges();
         }
         }
 
 
@@ -69,7 +69,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         foreach (var layerGuid in layerGuids)
         foreach (var layerGuid in layerGuids)
         {
         {
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta);
-            _tempChanges.Add(new LayerImageChunks_ChangeInfo(layerGuid, chunks));
+            _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
         }
         
         
         return _tempChanges;
         return _tempChanges;
@@ -83,7 +83,7 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, affected));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         }
         
         
         return changes;
         return changes;

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

@@ -102,9 +102,9 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
         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();
         memberImage.CancelChanges();
 
 
@@ -115,9 +115,9 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
         memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
         hasEnqueudImages = true;
         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)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -130,10 +130,10 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         foreach (var (guid, (image, pos)) in images!)
         {
         {
             ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
             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();
             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));
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
@@ -149,7 +149,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         foreach (var (guid, (image, pos)) in images!)
         {
         {
             ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
             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));
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
         return infos;
         return infos;
@@ -162,7 +162,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         {
         {
             var storageCopy = storage;
             var storageCopy = storage;
             var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, ref storageCopy);
             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!));
         (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);
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, shift);
             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();
             layer.LayerImage.CommitChanges();
         }
         }
 
 
@@ -86,7 +86,7 @@ internal class CenterContent_Change : Change
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
             var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageChunks_ChangeInfo(layerGuid, affected));
+            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         }
         
         
         return changes;
         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;
         bool flipY = flipType == FlipType.Vertical;
         
         
         flipped.DrawingSurface.Canvas.Save();
         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);
             flipY ? bounds.Y + (bounds.Height / 2f) : 0f);
         flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
         flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
         flipped.DrawingSurface.Canvas.Restore();
         flipped.DrawingSurface.Canvas.Restore();
@@ -100,13 +103,15 @@ internal sealed class FlipImage_Change : Change
                 {
                 {
                     FlipImage(layer.LayerImage);
                     FlipImage(layer.LayerImage);
                     changes.Add(
                     changes.Add(
-                        new LayerImageChunks_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                        new LayerImageArea_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedArea()));
                     layer.LayerImage.CommitChanges();
                     layer.LayerImage.CommitChanges();
                 }
                 }
 
 
                 if (member.Mask is not null)
                 if (member.Mask is not null)
                 {
                 {
                     FlipImage(member.Mask);
                     FlipImage(member.Mask);
+                    changes.Add(
+                        new MaskArea_ChangeInfo(member.GuidValue, member.Mask.FindAffectedArea()));
                     member.Mask.CommitChanges();
                     member.Mask.CommitChanges();
                 }
                 }
             }
             }

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

@@ -25,7 +25,7 @@ internal abstract class ResizeBasedChangeBase : Change
         img.EnqueueClear();
         img.EnqueueClear();
         img.EnqueueDrawChunkyImage(offset, img);
         img.EnqueueDrawChunkyImage(offset, img);
 
 
-        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, img.FindAffectedChunks()));
+        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, img.FindAffectedArea().Chunks));
         img.CommitChanges();
         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)
             if (member is Layer layer)
             {
             {
                 ScaleChunkyImage(layer.LayerImage);
                 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();
                 layer.LayerImage.CommitChanges();
             }
             }
             if (member.Mask is not null)
             if (member.Mask is not null)
             {
             {
                 ScaleChunkyImage(member.Mask);
                 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();
                 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.EnqueueClear();
         img.EnqueueDrawImage(bounds.Pos, flipped);
         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();
         img.CommitChanges();
     }
     }
 
 
@@ -215,7 +215,7 @@ internal sealed class RotateImage_Change : Change
             {
             {
                 layer.LayerImage.EnqueueResize(originalSize);
                 layer.LayerImage.EnqueueResize(originalSize);
                 deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
                 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();
                 layer.LayerImage.CommitChanges();
             }
             }
 
 
@@ -223,7 +223,7 @@ internal sealed class RotateImage_Change : Change
                 return;
                 return;
             member.Mask.EnqueueResize(originalSize);
             member.Mask.EnqueueResize(originalSize);
             deletedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
             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();
             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);
             surface.Canvas.Clear(Colors.White);
             if (previousImage != null)
             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));
             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)!;
         var layer = (Layer)target.FindMember(structureMemberGuid)!;
         layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
         layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
         ignoreInUndo = false;
         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();
         layer.LayerImage.CommitChanges();
         return layerInfo;
         return layerInfo;
@@ -40,6 +40,6 @@ internal sealed class ApplyMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
-        return new LayerImageChunks_ChangeInfo(structureMemberGuid, affected);
+        return new LayerImageArea_ChangeInfo(structureMemberGuid, affected);
     }
     }
 }
 }

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

@@ -10,12 +10,23 @@ public static class ChunkRenderer
 {
 {
     private static readonly Paint ClippingPaint = new Paint() { BlendMode = BlendMode.DstIn };
     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? TransformClipRect(RectI? globalClippingRect, ChunkResolution resolution, VecI chunkPos)
+    {
+        if (globalClippingRect is not RectI rect)
+            return null;
+
+        double multiplier = resolution.Multiplier();
+        VecI pixelChunkPos = chunkPos * (int)(ChunkyImage.FullChunkSize * multiplier);
+        return (RectI?)rect.Scale(multiplier).Translate(-pixelChunkPos).RoundOutwards();
+    }
+
+    public static OneOf<Chunk, EmptyChunk> MergeWholeStructure(VecI chunkPos, ChunkResolution resolution, IReadOnlyFolder root, RectI? globalClippingRect = null)
     {
     {
         using RenderingContext context = new();
         using RenderingContext context = new();
         try
         try
         {
         {
-            return MergeFolderContents(context, chunkPos, resolution, root, new All());
+            RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
+            return MergeFolderContents(context, chunkPos, resolution, root, new All(), transformedClippingRect);
         }
         }
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
         {
         {
@@ -23,12 +34,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();
         using RenderingContext context = new();
         try
         try
         {
         {
-            return MergeFolderContents(context, chunkPos, resolution, root, members);
+            RectI? transformedClippingRect = TransformClipRect(globalClippingRect, resolution, chunkPos);
+            return MergeFolderContents(context, chunkPos, resolution, root, members, transformedClippingRect);
         }
         }
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
         {
         {
@@ -36,8 +48,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 (
         if (
             clippingChunk.IsT1 ||
             clippingChunk.IsT1 ||
@@ -50,6 +68,14 @@ public static class ChunkRenderer
         context.UpdateFromMember(layer);
         context.UpdateFromMember(layer);
 
 
         Chunk renderingResult = Chunk.Create(resolution);
         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))
         if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.DrawingSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
         {
             renderingResult.Dispose();
             renderingResult.Dispose();
@@ -64,23 +90,42 @@ public static class ChunkRenderer
         }
         }
 
 
         if (clippingChunk.IsT2)
         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);
         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;
         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)
         if (clippingChunk.IsT1 || !layer.IsVisible || layer.Opacity == 0)
             return new EmptyChunk();
             return new EmptyChunk();
 
 
         if (layer.Mask is not null && layer.MaskIsVisible)
         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);
         context.UpdateFromMember(layer);
         Chunk renderingResult = Chunk.Create(resolution);
         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))
         if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.DrawingSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
         {
             renderingResult.Dispose();
             renderingResult.Dispose();
@@ -88,19 +133,31 @@ public static class ChunkRenderer
         }
         }
 
 
         if (clippingChunk.IsT2)
         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);
         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;
         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)
         if (clippingChunk.IsT1 || !layer.IsVisible || layer.Opacity == 0)
             return;
             return;
         if (layer.Mask is not null && layer.MaskIsVisible)
         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)
             if (result.IsT1)
                 result.AsT1.Dispose();
                 result.AsT1.Dispose();
             return;
             return;
@@ -108,13 +165,21 @@ public static class ChunkRenderer
         // clipping chunk requires a temp chunk anyway so we could as well reuse the code from RenderLayerSaveResult
         // clipping chunk requires a temp chunk anyway so we could as well reuse the code from RenderLayerSaveResult
         if (clippingChunk.IsT2)
         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)
             if (result.IsT1)
                 result.AsT1.Dispose();
                 result.AsT1.Dispose();
             return;
             return;
         }
         }
         context.UpdateFromMember(layer);
         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);
         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(
     private static OneOf<EmptyChunk, Chunk> RenderFolder(
@@ -124,7 +189,8 @@ public static class ChunkRenderer
         ChunkResolution resolution,
         ChunkResolution resolution,
         IReadOnlyFolder folder,
         IReadOnlyFolder folder,
         OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
         OneOf<FilledChunk, EmptyChunk, Chunk> clippingChunk,
-        OneOf<All, HashSet<Guid>> membersToMerge)
+        OneOf<All, HashSet<Guid>> membersToMerge,
+        RectI? transformedClippingRect)
     {
     {
         if (
         if (
             clippingChunk.IsT1 ||
             clippingChunk.IsT1 ||
@@ -135,11 +201,19 @@ public static class ChunkRenderer
         )
         )
             return new EmptyChunk();
             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)
         if (maybeContents.IsT1)
             return new EmptyChunk();
             return new EmptyChunk();
         Chunk contents = maybeContents.AsT0;
         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 is not null && folder.MaskIsVisible)
         {
         {
             if (!folder.Mask.DrawMostUpToDateChunkOn(chunkPos, resolution, contents.Surface.DrawingSurface, VecI.Zero, ClippingPaint))
             if (!folder.Mask.DrawMostUpToDateChunkOn(chunkPos, resolution, contents.Surface.DrawingSurface, VecI.Zero, ClippingPaint))
@@ -151,11 +225,17 @@ public static class ChunkRenderer
         }
         }
 
 
         if (clippingChunk.IsT2)
         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);
         context.UpdateFromMember(folder);
         contents.Surface.DrawingSurface.Canvas.DrawSurface(contents.Surface.DrawingSurface, 0, 0, context.ReplacingPaintWithOpacity);
         contents.Surface.DrawingSurface.Canvas.DrawSurface(contents.Surface.DrawingSurface, 0, 0, context.ReplacingPaintWithOpacity);
         targetChunk.Surface.DrawingSurface.Canvas.DrawSurface(contents.Surface.DrawingSurface, 0, 0, context.BlendModePaint);
         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;
         return contents;
     }
     }
 
 
@@ -164,7 +244,8 @@ public static class ChunkRenderer
         VecI chunkPos,
         VecI chunkPos,
         ChunkResolution resolution,
         ChunkResolution resolution,
         IReadOnlyFolder folder,
         IReadOnlyFolder folder,
-        OneOf<All, HashSet<Guid>> membersToMerge)
+        OneOf<All, HashSet<Guid>> membersToMerge,
+        RectI? transformedClippingRect)
     {
     {
         if (folder.Children.Count == 0)
         if (folder.Children.Count == 0)
             return new EmptyChunk();
             return new EmptyChunk();
@@ -196,12 +277,12 @@ public static class ChunkRenderer
             {
             {
                 if (needToSaveClippingChunk)
                 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;
                     clippingChunk = result.IsT0 ? result.AsT0 : result.AsT1;
                 }
                 }
                 else
                 else
                 {
                 {
-                    RenderLayer(context, targetChunk, chunkPos, resolution, layer, clippingChunk);
+                    RenderLayer(context, targetChunk, chunkPos, resolution, layer, clippingChunk, transformedClippingRect);
                 }
                 }
                 continue;
                 continue;
             }
             }
@@ -217,12 +298,12 @@ public static class ChunkRenderer
                 OneOf<All, HashSet<Guid>> innerMembersToMerge = shouldRenderAllChildren ? new All() : membersToMerge;
                 OneOf<All, HashSet<Guid>> innerMembersToMerge = shouldRenderAllChildren ? new All() : membersToMerge;
                 if (needToSaveClippingChunk)
                 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;
                     clippingChunk = result.IsT0 ? result.AsT0 : result.AsT1;
                 }
                 }
                 else
                 else
                 {
                 {
-                    RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge);
+                    RenderFolder(context, targetChunk, chunkPos, resolution, innerFolder, clippingChunk, innerMembersToMerge, transformedClippingRect);
                 }
                 }
             }
             }
         }
         }

+ 33 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs

@@ -206,6 +206,39 @@ public struct RectD : IEquatable<RectD>
         };
         };
     }
     }
 
 
+    public readonly RectD Scale(double multiplier)
+    {
+        return new RectD()
+        {
+            Left = left * multiplier,
+            Right = right * multiplier,
+            Top = top * multiplier,
+            Bottom = bottom * multiplier
+        };
+    }
+
+    public readonly RectD Scale(double multiplier, VecD relativeTo)
+    {
+        return new RectD()
+        {
+            Left = (left - relativeTo.X) * multiplier + relativeTo.X,
+            Right = (right - relativeTo.X) * multiplier + relativeTo.X,
+            Top = (top - relativeTo.Y) * multiplier + relativeTo.Y,
+            Bottom = (bottom - relativeTo.Y) * multiplier + relativeTo.Y
+        };
+    }
+
+    public readonly RectD Translate(VecD delta)
+    {
+        return new RectD()
+        {
+            Left = left + delta.X,
+            Right = right + delta.X,
+            Top = top + delta.Y,
+            Bottom = bottom + delta.Y
+        };
+    }
+
     /// <summary>
     /// <summary>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>
     /// </summary>

+ 45 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 
 
 namespace PixiEditor.DrawingApi.Core.Numerics;
 namespace PixiEditor.DrawingApi.Core.Numerics;
@@ -193,6 +194,50 @@ public struct RectI : IEquatable<RectI>
         };
         };
     }
     }
 
 
+    public readonly RectD Scale(double multiplier)
+    {
+        return new RectD()
+        {
+            Left = left * multiplier,
+            Right = right * multiplier,
+            Top = top * multiplier,
+            Bottom = bottom * multiplier
+        };
+    }
+
+    public readonly RectD Scale(double multiplier, VecD relativeTo)
+    {
+        return new RectD()
+        {
+            Left = (left - relativeTo.X) * multiplier + relativeTo.X,
+            Right = (right - relativeTo.X) * multiplier + relativeTo.X,
+            Top = (top - relativeTo.Y) * multiplier + relativeTo.Y,
+            Bottom = (bottom - relativeTo.Y) * multiplier + relativeTo.Y
+        };
+    }
+
+    public readonly RectI Translate(VecI delta)
+    {
+        return new RectI()
+        {
+            Left = left + delta.X,
+            Right = right + delta.X,
+            Top = top + delta.Y,
+            Bottom = bottom + delta.Y
+        };
+    }
+
+    public readonly RectD Translate(VecD delta)
+    {
+        return new RectD()
+        {
+            Left = left + delta.X,
+            Right = right + delta.X,
+            Top = top + delta.Y,
+            Bottom = bottom + delta.Y
+        };
+    }
+
     /// <summary>
     /// <summary>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>
     /// </summary>

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

@@ -15,24 +15,22 @@ namespace PixiEditor.Models.DocumentModels;
 #nullable enable
 #nullable enable
 internal class ActionAccumulator
 internal class ActionAccumulator
 {
 {
-    private const long minMsPerUpdate = 1000 / 60;
-    private Stopwatch updateStopwatch = Stopwatch.StartNew();
-    private long lastUpdateMs = 0;
-
     private bool executing = false;
     private bool executing = false;
 
 
     private List<IAction> queuedActions = new();
     private List<IAction> queuedActions = new();
     private DocumentViewModel document;
     private DocumentViewModel document;
     private DocumentInternalParts internals;
     private DocumentInternalParts internals;
 
 
-    private WriteableBitmapUpdater renderer;
+    private CanvasUpdater canvasUpdater;
+    private MemberPreviewUpdater previewUpdater;
 
 
     public ActionAccumulator(DocumentViewModel doc, DocumentInternalParts internals)
     public ActionAccumulator(DocumentViewModel doc, DocumentInternalParts internals)
     {
     {
         this.document = doc;
         this.document = doc;
         this.internals = internals;
         this.internals = internals;
 
 
-        renderer = new(doc, internals);
+        canvasUpdater = new(doc, internals);
+        previewUpdater = new(doc, internals);
     }
     }
 
 
     public void AddFinishedActions(params IAction[] actions)
     public void AddFinishedActions(params IAction[] actions)
@@ -63,13 +61,6 @@ internal class ActionAccumulator
 
 
         while (queuedActions.Count > 0)
         while (queuedActions.Count > 0)
         {
         {
-            // wait to limit update rate
-            long currentMillis = updateStopwatch.ElapsedMilliseconds;
-            long waitDuration = minMsPerUpdate - (currentMillis - lastUpdateMs);
-            if (waitDuration > 0)
-                await Task.Delay((int)waitDuration);
-            lastUpdateMs = updateStopwatch.ElapsedMilliseconds;
-
             // select actions to be processed
             // select actions to be processed
             var toExecute = queuedActions;
             var toExecute = queuedActions;
             queuedActions = new List<IAction>();
             queuedActions = new List<IAction>();
@@ -111,9 +102,11 @@ internal class ActionAccumulator
             // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
             // Also, there is a bug report for this on github https://github.com/dotnet/wpf/issues/5816
 
 
             // update the contents of the bitmaps
             // 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
             // lock bitmaps
             foreach (var (_, bitmap) in document.LazyBitmaps)
             foreach (var (_, bitmap) in document.LazyBitmaps)
             {
             {

+ 54 - 37
src/PixiEditor/Models/Rendering/AffectedChunkGatherer.cs → src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -1,4 +1,5 @@
-using ChunkyImageLib;
+using System.ComponentModel.DataAnnotations;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -11,15 +12,15 @@ using PixiEditor.DrawingApi.Core.Numerics;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
 #nullable enable
 #nullable enable
-internal class AffectedChunkGatherer
+internal class AffectedAreasGatherer
 {
 {
     private readonly DocumentChangeTracker tracker;
     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;
         this.tracker = tracker;
         ProcessChanges(changes);
         ProcessChanges(changes);
@@ -31,18 +32,18 @@ internal class AffectedChunkGatherer
         {
         {
             switch (change)
             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");
                         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;
                     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");
                         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;
                     break;
                 case CreateStructureMember_ChangeInfo info:
                 case CreateStructureMember_ChangeInfo info:
                     AddAllToMainImage(info.GuidValue);
                     AddAllToMainImage(info.GuidValue);
@@ -99,7 +100,7 @@ internal class AffectedChunkGatherer
         if (member is IReadOnlyLayer layer)
         if (member is IReadOnlyLayer layer)
         {
         {
             var chunks = layer.LayerImage.FindAllChunks();
             var chunks = layer.LayerImage.FindAllChunks();
-            AddToImagePreviews(memberGuid, chunks, ignoreSelf);
+            AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         }
         else if (member is IReadOnlyFolder folder)
         else if (member is IReadOnlyFolder folder)
         {
         {
@@ -117,7 +118,7 @@ internal class AffectedChunkGatherer
             var chunks = layer.LayerImage.FindAllChunks();
             var chunks = layer.LayerImage.FindAllChunks();
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
-            AddToMainImage(chunks);
+            AddToMainImage(new AffectedArea(chunks));
         }
         }
         else
         else
         {
         {
@@ -132,7 +133,7 @@ internal class AffectedChunkGatherer
         if (member.Mask is not null)
         if (member.Mask is not null)
         {
         {
             var chunks = member.Mask.FindAllChunks();
             var chunks = member.Mask.FindAllChunks();
-            AddToMaskPreview(memberGuid, chunks);
+            AddToMaskPreview(memberGuid, new AffectedArea(chunks));
         }
         }
         if (member is IReadOnlyFolder folder)
         if (member is IReadOnlyFolder folder)
         {
         {
@@ -142,12 +143,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);
         var path = tracker.Document.FindMemberPath(memberGuid);
         if (path.Count < 2)
         if (path.Count < 2)
@@ -155,25 +158,37 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
         {
             var member = path[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
             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
         else
-            MaskPreviewChunks[memberGuid].UnionWith(chunks);
+        {
+            var temp = MaskPreviewAreas[memberGuid];
+            temp.UnionWith(area);
+            MaskPreviewAreas[memberGuid] = temp;
+        }
     }
     }
 
 
 
 
     private void AddWholeCanvasToMainImage()
     private void AddWholeCanvasToMainImage()
     {
     {
-        AddAllChunks(MainImageChunks);
+        MainImageArea = AddWholeArea(MainImageArea);
     }
     }
 
 
     private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
     private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
@@ -185,17 +200,17 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
         {
             var member = path[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();
+            ImagePreviewAreas[member.GuidValue] = AddWholeArea(ImagePreviewAreas[member.GuidValue]);
         }
         }
     }
     }
 
 
     private void AddWholeCanvasToMaskPreview(Guid memberGuid)
     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();
+        MaskPreviewAreas[memberGuid] = AddWholeArea(MaskPreviewAreas[memberGuid]);
     }
     }
 
 
 
 
@@ -209,7 +224,7 @@ internal class AffectedChunkGatherer
         tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
         tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
     }
     }
 
 
-    private void AddAllChunks(HashSet<VecI> chunks)
+    private AffectedArea AddWholeArea(AffectedArea area)
     {
     {
         VecI size = new(
         VecI size = new(
             (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
             (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
@@ -218,8 +233,10 @@ internal class AffectedChunkGatherer
         {
         {
             for (int j = 0; j < size.Y; j++)
             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);
+        return area;
     }
     }
 }
 }

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

@@ -0,0 +1,215 @@
+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, AffectedArea chunkGathererAffectedArea)
+    {
+        if (chunkGathererAffectedArea.Chunks.Count > 0)
+        {
+            foreach (var (res, chunks) in chunksToRerender)
+            {
+                affectedAndNonRerenderedChunks[res].UnionWith(chunkGathererAffectedArea.Chunks);
+            }
+        }
+
+        foreach (var (res, chunks) in chunksToRerender)
+        {
+            affectedAndNonRerenderedChunks[res].ExceptWith(chunks);
+            nonRerenderedChunksAffectedBeforeLastRerenderDelayed[res].ExceptWith(chunks);
+        }
+    }
+
+    private List<IRenderInfo> Render(AffectedAreasGatherer chunkGatherer, bool rerenderDelayed)
+    {
+        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;
+            }
+        }
+
+        bool anythingToUpdate = false;
+        foreach (var (_, chunks) in chunksToRerender)
+        {
+            anythingToUpdate |= chunks.Count > 0;
+        }
+        if (!anythingToUpdate)
+            return new();
+
+        UpdateAffectedNonRerenderedChunks(chunksToRerender, chunkGatherer.MainImageArea);
+        
+        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();
+            RectI? globalScaledClippingRectangle = null;
+            if (globalClippingRectangle is not null)
+                globalScaledClippingRectangle = (RectI?)((RectI)globalClippingRectangle).Scale(resolution.Multiplier()).RoundOutwards();
+
+            DrawingSurface screenSurface = doc.Surfaces[resolution];
+            foreach (var chunkPos in chunks)
+            {
+                RenderChunk(chunkPos, screenSurface, resolution, globalClippingRectangle, globalScaledClippingRectangle);
+                RectI chunkRect = new(chunkPos * chunkSize, new(chunkSize, chunkSize));
+                if (globalScaledClippingRectangle is RectI rect)
+                    chunkRect = chunkRect.Intersect(rect);
+
+                infos.Add(new DirtyRect_RenderInfo(
+                    chunkRect.Pos,
+                    chunkRect.Size,
+                    resolution
+                ));
+            }
+        }
+    }
+
+    private void RenderChunk(VecI chunkPos, DrawingSurface screenSurface, ChunkResolution resolution, RectI? globalClippingRectangle, RectI? globalScaledClippingRectangle)
+    {
+        if (globalScaledClippingRectangle is not null)
+        {
+            screenSurface.Canvas.Save();
+            screenSurface.Canvas.ClipRect((RectD)globalScaledClippingRectangle);
+        }
+
+        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 (globalScaledClippingRectangle 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)
             if (scope == DocumentScope.AllLayers)
             {
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 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>(
                     .Match<Color>(
                         (Chunk chunk) =>
                         (Chunk chunk) =>
                         {
                         {