Explorar o código

Merge branch 'master' into development

Krzysztof Krysiński %!s(int64=2) %!d(string=hai) anos
pai
achega
d3775e5447
Modificáronse 100 ficheiros con 1838 adicións e 838 borrados
  1. 11 9
      README.md
  2. 1 0
      src/ChunkyImageLib/Chunk.cs
  3. 52 36
      src/ChunkyImageLib/ChunkyImage.cs
  4. 107 0
      src/ChunkyImageLib/DataHolders/AffectedArea.cs
  5. 50 2
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  6. 1 1
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  7. 3 3
      src/ChunkyImageLib/Operations/ApplyMaskOperation.cs
  8. 2 2
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  9. 3 2
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  10. 2 2
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  11. 4 3
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  12. 2 2
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  13. 2 2
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  14. 1 1
      src/ChunkyImageLib/Operations/IDrawOperation.cs
  15. 8 2
      src/ChunkyImageLib/Operations/ImageOperation.cs
  16. 45 1
      src/ChunkyImageLib/Operations/OperationHelper.cs
  17. 2 2
      src/ChunkyImageLib/Operations/PathOperation.cs
  18. 2 2
      src/ChunkyImageLib/Operations/PixelOperation.cs
  19. 15 3
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  20. 6 3
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  21. 3 2
      src/ChunkyImageLib/Operations/ReplaceColorOperation.cs
  22. 1 0
      src/ChunkyImageLibTest/ChunkyImageLibTest.csproj
  23. 6 14
      src/ChunkyImageLibTest/ChunkyImageTests.cs
  24. 2 2
      src/ChunkyImageLibTest/ClearRegionOperationTests.cs
  25. 12 1
      src/ChunkyImageLibTest/ImageOperationTests.cs
  26. 14 14
      src/ChunkyImageLibTest/RectangleOperationTests.cs
  27. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/LayerImageArea_ChangeInfo.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/MaskArea_ChangeInfo.cs
  29. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  30. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  31. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  32. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  33. 12 12
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawEllipse_UpdateableChange.cs
  34. 7 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawLine_UpdateableChange.cs
  35. 12 12
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  36. 7 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  37. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  38. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  39. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  40. 9 9
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PathBasedPen_UpdateableChange.cs
  41. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs
  42. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ReplaceColor_Change.cs
  43. 6 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs
  44. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  45. 19 18
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  46. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  47. 7 2
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  48. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  49. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  50. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  51. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandVisualizer.cs
  52. 1 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs
  53. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs
  54. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Structure/ApplyMask_Change.cs
  55. 11 0
      src/PixiEditor.ChangeableDocument/Enums/FlipType.cs
  56. 104 23
      src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs
  57. 1 1
      src/PixiEditor.DrawingApi.Core/Bridge/DrawingBackendApi.cs
  58. 33 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs
  59. 46 1
      src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs
  60. 7 0
      src/PixiEditor.DrawingApi.Core/Numerics/VecD.cs
  61. 20 1
      src/PixiEditor/Helpers/Behaviours/SliderUpdateBehavior.cs
  62. 1 8
      src/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs
  63. 39 0
      src/PixiEditor/Helpers/Collections/ActionDisplayList.cs
  64. 39 0
      src/PixiEditor/Helpers/ColorHelper.cs
  65. 27 0
      src/PixiEditor/Helpers/Converters/EmptyStringFillerConverter.cs
  66. 13 1
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  67. 7 4
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  68. 26 0
      src/PixiEditor/Helpers/FocusHelper.cs
  69. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png
  70. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180DegLayers.png
  71. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270Deg.png
  72. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270DegLayers.png
  73. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90Deg.png
  74. BIN=BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90DegLayers.png
  75. 12 6
      src/PixiEditor/Models/Commands/CommandController.cs
  76. 34 23
      src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs
  77. 38 12
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  78. 2 0
      src/PixiEditor/Models/Dialogs/ExportFileDialog.cs
  79. 9 16
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  80. 8 0
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  81. 10 6
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  82. 182 3
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  83. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LassoToolExecutor.cs
  84. 29 10
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs
  85. 34 14
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  86. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SelectToolExecutor.cs
  87. 22 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs
  88. 8 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs
  89. 11 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs
  90. 3 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs
  91. 54 37
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  92. 215 0
      src/PixiEditor/Models/Rendering/CanvasUpdater.cs
  93. 217 0
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  94. 0 310
      src/PixiEditor/Models/Rendering/WriteableBitmapUpdater.cs
  95. 13 1
      src/PixiEditor/PixiEditor.csproj
  96. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  97. 11 42
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  98. 5 3
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs
  99. 22 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  100. 0 41
      src/PixiEditor/ViewModels/SubViewModels/Document/LineToolOverlayViewModel.cs

+ 11 - 9
README.md

@@ -25,21 +25,23 @@ Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we w
 ![](https://user-images.githubusercontent.com/45312141/146670495-ae521a18-a89e-4e94-9317-6838b51407fa.png)
 
 
+### Fast
 
-### Lightweight
-
-The program weighs only 9 MB! Already have .NET 6 installed? Download installer and enjoy saved space.
+PixiEditor is fast, drawing feels smooth on any canvas size, we've developed original chunk-based system and adaptive rendering to minimize pixel processing time.
 
 ### Active development
 
 PixiEditor started in 2018 and it's been actively developed since. We continuously improve code quality to ensure the best experience and performance.
 
 
-
 ## Installation
 
 <a href='//www.microsoft.com/store/apps/9NDDRHS8PBRN?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='Microsoft Store badge' width="184"/></a>
 
+Wishlist on Steam now!
+
+[![wishlist_steam](https://user-images.githubusercontent.com/25402427/214952291-d81a4d79-bb75-44f2-bd24-10d7c3404997.png)](https://store.steampowered.com/app/2218560/PixiEditor__Pixel_Art_Editor?utm_source=GitHub)
+
 **Or**
 
 Follow these instructions to get PixiEditor working on your machine.
@@ -52,9 +54,9 @@ Follow these instructions to get PixiEditor working on your machine.
 
 ## Featured content
 
-### PixiEditor 0.1.4 Trailer
+### PixiEditor 1.0 Trailer
 
-[![Trailer](https://img.youtube.com/vi/QKnXBUY0Pqk/0.jpg)](https://www.youtube.com/watch?v=QKnXBUY0Pqk)
+[![Trailer](https://img.youtube.com/vi/UK8HnrAQhCo/0.jpg)](https://www.youtube.com/watch?v=UK8HnrAQhCo)
 
 ### Pixel Art Timelapse - "Bog Landscape" | PixiEditor
 
@@ -81,15 +83,15 @@ Struggling with something? You can find support in a few places:
 
 ### Software Requirements
 
-* .NET 5
+* .NET 7
 
-* Visual Studio
+* latest Visual Studio 2022 (in order to code generators to work)
 
 ### Instructions
 
 1. Clone Repository
 
-2. Open PixiEditor/PixiEditor/PixiEditor.sln in Visual Studio
+2. Open PixiEditor/src/PixiEditor/PixiEditor.sln in Visual Studio
 
 3. Build solution
 

+ 1 - 0
src/ChunkyImageLib/Chunk.cs

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

+ 52 - 36
src/ChunkyImageLib/ChunkyImage.cs

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

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

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

+ 50 - 2
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -55,7 +55,8 @@ public struct ShapeCorners
     {
         get
         {
-            Span<VecD> lengths = stackalloc[] {
+            Span<VecD> lengths = stackalloc[] 
+            {
                 TopLeft - TopRight,
                 TopRight - BottomRight,
                 BottomRight - BottomLeft,
@@ -91,6 +92,18 @@ public struct ShapeCorners
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
         }
     }
+    public RectD AABBBounds
+    {
+        get
+        {
+            double minX = Math.Min(Math.Min(TopLeft.X, TopRight.X), Math.Min(BottomLeft.X, BottomRight.X));
+            double minY = Math.Min(Math.Min(TopLeft.Y, TopRight.Y), Math.Min(BottomLeft.Y, BottomRight.Y));
+            double maxX = Math.Max(Math.Max(TopLeft.X, TopRight.X), Math.Max(BottomLeft.X, BottomRight.X));
+            double maxY = Math.Max(Math.Max(TopLeft.Y, TopRight.Y), Math.Max(BottomLeft.Y, BottomRight.Y));
+            return RectD.FromTwoPoints(new VecD(minX, minY), new VecD(maxX, maxY));
+        }
+    }
+
     public bool IsPointInside(VecD point)
     {
         var top = TopLeft - TopRight;
@@ -121,7 +134,7 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectY(horAxisY),
         TopRight = TopRight.ReflectY(horAxisY)
     };
-    
+
     public ShapeCorners AsMirroredAcrossVerAxis(int verAxisX) => new ShapeCorners
     {
         BottomLeft = BottomLeft.ReflectX(verAxisX),
@@ -129,4 +142,39 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopRight = TopRight.ReflectX(verAxisX)
     };
+
+    public ShapeCorners AsRotated(double angle, VecD around) => new ShapeCorners
+    {
+        BottomLeft = BottomLeft.Rotate(angle, around),
+        BottomRight = BottomRight.Rotate(angle, around),
+        TopLeft = TopLeft.Rotate(angle, around),
+        TopRight = TopRight.Rotate(angle, around)
+    };
+
+    public ShapeCorners AsTranslated(VecD delta) => new ShapeCorners
+    {
+        BottomLeft = BottomLeft + delta,
+        BottomRight = BottomRight + delta,
+        TopLeft = TopLeft + delta,
+        TopRight = TopRight + delta
+    };
+
+    public static bool operator !=(ShapeCorners left, ShapeCorners right) => !(left == right);
+    public static bool operator == (ShapeCorners left, ShapeCorners right)
+    {
+        return 
+           left.TopLeft == right.TopLeft &&
+           left.TopRight == right.TopRight &&
+           left.BottomLeft == right.BottomLeft &&
+           left.BottomRight == right.BottomRight;
+    }
+
+    public bool AlmostEquals(ShapeCorners other, double epsilon = 0.001)
+    {
+        return
+            TopLeft.AlmostEquals(other.TopLeft, epsilon) &&
+            TopRight.AlmostEquals(other.TopRight, epsilon) &&
+            BottomLeft.AlmostEquals(other.BottomLeft, epsilon) &&
+            BottomRight.AlmostEquals(other.BottomRight, epsilon);
+    }
 }

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 0
src/ChunkyImageLibTest/ChunkyImageLibTest.csproj

@@ -22,6 +22,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj" />
+    <ProjectReference Include="..\PixiEditor.DrawingApi.Skia\PixiEditor.DrawingApi.Skia.csproj" />
   </ItemGroup>
 
 </Project>

+ 6 - 14
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -1,30 +1,22 @@
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Skia;
 using Xunit;
 
 namespace ChunkyImageLibTest;
 public class ChunkyImageTests
 {
-    public static Surface ImportImage(string path, VecI size)
+    public ChunkyImageTests()
     {
-        Surface original = Surface.Load(path);
-        if (original.Size != size)
+        try
         {
-            Surface resized = original.ResizeNearestNeighbor(size);
-            original.Dispose();
-            return resized;
+            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend());
         }
-        return original;
-    }
-
-    [Fact]
-    public void LoadDemo()
-    {
-        var path = @"C:\Users\egor0\Desktop\SpazzS1.png";
-        ImportImage(path, new VecI(5, 5));
+        catch { }
     }
 
     [Fact]

+ 2 - 2
src/ChunkyImageLibTest/ClearRegionOperationTests.cs

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

+ 12 - 1
src/ChunkyImageLibTest/ImageOperationTests.cs

@@ -2,18 +2,29 @@
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Skia;
 using Xunit;
 
 namespace ChunkyImageLibTest;
 public class ImageOperationTests
 {
+    public ImageOperationTests()
+    {
+        try
+        {
+            DrawingBackendApi.SetupBackend(new SkiaDrawingBackend());
+        }
+        catch { }
+    }
+
     [Fact]
     public void FindAffectedChunks_SingleChunk_ReturnsSingleChunk()
     {
         using Surface testImage = new Surface((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         using ImageOperation operation = new((ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), testImage);
-        var chunks = operation.FindAffectedChunks(new(ChunkyImage.FullChunkSize));
+        var chunks = operation.FindAffectedArea(new(ChunkyImage.FullChunkSize)).Chunks;
         Assert.Equal(new HashSet<VecI>() { new(1, 1) }, chunks);
     }
 }

+ 14 - 14
src/ChunkyImageLibTest/RectangleOperationTests.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 19 - 18
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -15,9 +15,10 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
 
     private Dictionary<Guid, (Surface surface, VecI pos)>? images;
     private Matrix3X3 globalMatrix;
-    private RectI originalTightBounds;
     private Dictionary<Guid, CommittedChunkStorage>? savedChunks;
 
+    private RectD originalTightBounds;
+    private RectI roundedTightBounds;
     private VectorPath? originalPath;
 
     private bool hasEnqueudImages = false;
@@ -49,21 +50,21 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         }
 
         originalPath = new VectorPath(target.Selection.SelectionPath) { FillType = PathFillType.EvenOdd };
-        RectI bounds = (RectI)originalPath.TightBounds;
+        
+        originalTightBounds = originalPath.TightBounds;
+        roundedTightBounds = (RectI)originalTightBounds.RoundOutwards();
+        //boundsRoundingOffset = bounds.TopLeft - roundedBounds.TopLeft;
 
         images = new();
         foreach (var guid in membersToTransform)
         {
             ChunkyImage image = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
-            var extracted = ExtractArea(image, originalPath, bounds);
+            var extracted = ExtractArea(image, originalPath, roundedTightBounds);
             if (extracted.IsT0)
                 continue;
             images.Add(guid, (extracted.AsT1.image, extracted.AsT1.extractedRect.Pos));
         }
 
-        if (images.Count == 0)
-            return false;
-        originalTightBounds = bounds;
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
         return true;
     }
@@ -101,22 +102,22 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
     }
 
-    private HashSet<VecI> DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
+    private AffectedArea DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
     {
-        var prevChunks = memberImage.FindAffectedChunks();
+        var prevAffArea = memberImage.FindAffectedArea();
 
         memberImage.CancelChanges();
 
         if (!keepOriginal)
-            memberImage.EnqueueClearPath(originalPath!, originalTightBounds);
-        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - originalTightBounds.Left, originalPos.Y - originalTightBounds.Top);
+            memberImage.EnqueueClearPath(originalPath!, roundedTightBounds);
+        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - (float)originalTightBounds.Left, originalPos.Y - (float)originalTightBounds.Top);
         localMatrix = localMatrix.PostConcat(globalMatrix);
         memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
         hasEnqueudImages = true;
 
-        var affectedChunks = memberImage.FindAffectedChunks();
-        affectedChunks.UnionWith(prevChunks);
-        return affectedChunks;
+        var affectedArea = memberImage.FindAffectedArea();
+        affectedArea.UnionWith(prevAffArea);
+        return affectedArea;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -129,10 +130,10 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         {
             ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
-            var chunks = DrawImage(target, guid, image, pos, memberImage);
-            savedChunks[guid] = new(memberImage, memberImage.FindAffectedChunks());
+            var area = DrawImage(target, guid, image, pos, memberImage);
+            savedChunks[guid] = new(memberImage, memberImage.FindAffectedArea().Chunks);
             memberImage.CommitChanges();
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, area, drawOnMask).AsT1);
         }
 
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
@@ -148,7 +149,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, (image, pos)) in images!)
         {
             ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask);
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
         }
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
         return infos;
@@ -161,7 +162,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         {
             var storageCopy = storage;
             var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, ref storageCopy);
-            infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
+            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, chunks, drawOnMask).AsT1);
         }
 
         (var toDispose, target.Selection.SelectionPath) = (target.Selection.SelectionPath, new VectorPath(originalPath!));

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectionChangeHelper.cs

@@ -6,14 +6,13 @@ namespace PixiEditor.ChangeableDocument.Changes.Selection;
 internal class SelectionChangeHelper
 {
     public static Selection_ChangeInfo DoSelectionTransform(
-        Document target, VectorPath originalPath, RectI originalPathTightBounds, ShapeCorners to)
+        Document target, VectorPath originalPath, RectD originalPathTightBounds, ShapeCorners to)
     {
         VectorPath newPath = new(originalPath);
 
         var matrix = Matrix3X3.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y).PostConcat(
             OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
         newPath.Transform(matrix);
-
         var toDispose = target.Selection.SelectionPath;
         target.Selection.SelectionPath = newPath;
         toDispose.Dispose();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/TransformSelectionPath_UpdateableChange.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.ChangeableDocument.Changes.Selection;
 internal class TransformSelectionPath_UpdateableChange : UpdateableChange
 {
     private VectorPath? originalPath;
-    private RectI originalTightBounds;
+    private RectD originalTightBounds;
     private ShapeCorners newCorners;
 
     [GenerateUpdateableChangeActions]
@@ -25,7 +25,7 @@ internal class TransformSelectionPath_UpdateableChange : UpdateableChange
         if (target.Selection.SelectionPath.IsEmpty)
             return false;
         originalPath = new(target.Selection.SelectionPath);
-        originalTightBounds = (RectI)originalPath.TightBounds;
+        originalTightBounds = originalPath.TightBounds;
         return true;
     }
 

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

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

+ 11 - 0
src/PixiEditor.ChangeableDocument/Enums/FlipType.cs

@@ -11,7 +11,18 @@ public enum FlipType
 /// </summary>
 public enum RotationAngle
 {
+    /// <summary>
+    /// 90 Degree
+    /// </summary>
     D90,
+    
+    /// <summary>
+    /// 180 Degree
+    /// </summary>
     D180,
+
+    /// <summary>
+    /// -90 Degree
+    /// </summary>
     D270
 }

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

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Bridge/DrawingBackendApi.cs

@@ -5,7 +5,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge
 {
     public static class DrawingBackendApi
     {
-        private static IDrawingBackend _current;
+        private static IDrawingBackend? _current;
 
         public static IDrawingBackend Current
         {

+ 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>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>

+ 46 - 1
src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Reflection;
 using System.Runtime.InteropServices;
 
 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>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>
@@ -200,7 +245,7 @@ public struct RectI : IEquatable<RectI>
     {
         RectD rectD = (RectD)rect;
         RectD thisD = (RectD)this;
-        RectD aspect = rectD.AspectFit(thisD);
+        RectD aspect = thisD.AspectFit(rectD);
         return new RectI((int)aspect.Left, (int)aspect.Top, (int)aspect.Width, (int)aspect.Height);
     }
 

+ 7 - 0
src/PixiEditor.DrawingApi.Core/Numerics/VecD.cs

@@ -224,4 +224,11 @@ public struct VecD : IEquatable<VecD>
     {
         return other.X == X && other.Y == Y;
     }
+
+    public bool AlmostEquals(VecD other, double axisEpsilon = 0.001)
+    {
+        double dX = Math.Abs(X - other.X);
+        double dY = Math.Abs(Y - other.Y);
+        return dX < axisEpsilon && dY < axisEpsilon;
+    }
 }

+ 20 - 1
src/PixiEditor/Helpers/Behaviours/SliderUpdateBehavior.cs

@@ -3,7 +3,6 @@ using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Input;
 using Microsoft.Xaml.Behaviors;
-
 namespace PixiEditor.Helpers.Behaviours;
 #nullable enable
 internal class SliderUpdateBehavior : Behavior<Slider>
@@ -41,6 +40,15 @@ internal class SliderUpdateBehavior : Behavior<Slider>
         set => SetValue(DragStartedProperty, value);
     }
 
+    public static readonly DependencyProperty SetOpacityProperty =
+        DependencyProperty.Register(nameof(SetOpacity), typeof(ICommand), typeof(SliderUpdateBehavior), new(null));
+
+    public ICommand SetOpacity
+    {
+        get => (ICommand)GetValue(SetOpacityProperty);
+        set => SetValue(SetOpacityProperty, value);
+    }
+
     public static DependencyProperty ValueFromSliderProperty =
         DependencyProperty.Register(nameof(ValueFromSlider), typeof(double), typeof(SliderUpdateBehavior), new(OnSliderValuePropertyChange));
     public double ValueFromSlider
@@ -55,6 +63,8 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private bool bindingValueChangedWhileDragging = false;
     private double bindingValueWhileDragging = 0.0;
 
+    private bool skipSetOpacity;
+    
     protected override void OnAttached()
     {
         AssociatedObject.Loaded += AssociatedObject_Loaded;
@@ -101,23 +111,32 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private static void OnSliderValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        
         if (obj.dragging)
         {
             if (obj.DragValueChanged is not null && obj.DragValueChanged.CanExecute(e.NewValue))
                 obj.DragValueChanged.Execute(e.NewValue);
         }
+        else if (!obj.skipSetOpacity)
+        {
+            if (obj.SetOpacity is not null && obj.SetOpacity.CanExecute(e.NewValue))
+                obj.SetOpacity.Execute(e.NewValue);
+        }
     }
 
     private static void OnBindingValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        obj.skipSetOpacity = true;
         if (obj.dragging)
         {
             obj.bindingValueChangedWhileDragging = true;
             obj.bindingValueWhileDragging = (double)e.NewValue;
+            obj.skipSetOpacity = false;
             return;
         }
         obj.ValueFromSlider = (double)e.NewValue;
+        obj.skipSetOpacity = false;
     }
 
     private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)

+ 1 - 8
src/PixiEditor/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -87,14 +87,7 @@ internal class TextBoxFocusBehavior : Behavior<TextBox>
     {
         if (!FocusNext)
         {
-            FrameworkElement parent = (FrameworkElement)AssociatedObject.Parent;
-            while (parent is IInputElement elem && !elem.Focusable)
-            {
-                parent = (FrameworkElement)parent.Parent;
-            }
-
-            DependencyObject scope = FocusManager.GetFocusScope(AssociatedObject);
-            FocusManager.SetFocusedElement(scope, parent);
+            FocusHelper.MoveFocusToParent(AssociatedObject);
         }
         else
         {

+ 39 - 0
src/PixiEditor/Helpers/Collections/ActionDisplayList.cs

@@ -0,0 +1,39 @@
+using System.Collections;
+
+namespace PixiEditor.Helpers.Collections;
+
+public class ActionDisplayList : IEnumerable<KeyValuePair<string, string>>
+{
+    private Dictionary<string, string> _dictionary = new();
+    private Action notifyUpdate;
+
+    public ActionDisplayList(Action notifyUpdate)
+    {
+        this.notifyUpdate = notifyUpdate;
+    }
+
+    public string this[string key]
+    {
+        get => _dictionary[key];
+        set
+        {
+            if (value == null)
+            {
+                _dictionary.Remove(key);
+                notifyUpdate();
+                return;
+            }
+            
+            _dictionary[key] = value;
+            notifyUpdate();
+        }
+    }
+
+    public string GetActive() => _dictionary.Last().Value;
+
+    public bool HasActive() => _dictionary.Count != 0;
+
+    public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => _dictionary.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
+}

+ 39 - 0
src/PixiEditor/Helpers/ColorHelper.cs

@@ -0,0 +1,39 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using System.Windows;
+
+namespace PixiEditor.Helpers;
+
+public class ColorHelper
+{
+    public static bool ParseAnyFormat(IDataObject data, [NotNullWhen(true)] out DrawingApi.Core.ColorsImpl.Color? result) => 
+        ParseAnyFormat(((DataObject)data).GetText().Trim(), out result);
+    
+    public static bool ParseAnyFormat(string value, [NotNullWhen(true)] out DrawingApi.Core.ColorsImpl.Color? result)
+    {
+        bool hex = Regex.IsMatch(value, "^#?([a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$");
+
+        if (hex)
+        {
+            result = DrawingApi.Core.ColorsImpl.Color.Parse(value);
+            return true;
+        }
+
+        var match = Regex.Match(value, @"(?:rgba?\(?)? *(?<r>\d{1,3})(?:, *| +)(?<g>\d{1,3})(?:, *| +)(?<b>\d{1,3})(?:(?:, *| +)(?<a>\d{0,3}))?\)?");
+
+        if (!match.Success)
+        {
+            result = null;
+            return false;
+        }
+
+        byte r = byte.Parse(match.Groups["r"].ValueSpan);
+        byte g = byte.Parse(match.Groups["g"].ValueSpan);
+        byte b = byte.Parse(match.Groups["b"].ValueSpan);
+        byte a = match.Groups["a"].Success ? byte.Parse(match.Groups["a"].ValueSpan) : (byte)255;
+
+        result = new DrawingApi.Core.ColorsImpl.Color(r, g, b, a);
+        return true;
+
+    }
+}

+ 27 - 0
src/PixiEditor/Helpers/Converters/EmptyStringFillerConverter.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers.Converters;
+internal class EmptyStringFillerConverter : MarkupConverter
+{
+    public string NullText { get; set; } = "[null]";
+
+    public string EmptyText { get; set; } = "[empty]";
+
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return value switch
+        {
+            string s => s.Length switch
+            {
+                0 => EmptyText,
+                _ => s
+            },
+            _ => NullText
+        };
+    }
+}

+ 13 - 1
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -55,6 +55,8 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         public float Opacity { get; set; }
         
         public BlendMode BlendMode { get; set; }
+        
+        public bool ClipToMemberBelow { get; set; }
 
         public bool HasMask => maskBuilder is not null;
 
@@ -115,6 +117,12 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
             GuidValue = guid;
             return this;
         }
+
+        public StructureMemberBuilder WithClipToBelow(bool value)
+        {
+            ClipToMemberBelow = value;
+            return this;
+        }
     }
 
     public class LayerBuilder : StructureMemberBuilder
@@ -148,6 +156,8 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         
         public new LayerBuilder WithBlendMode(BlendMode blendMode) => base.WithBlendMode(blendMode) as LayerBuilder;
         
+        public new LayerBuilder WithClipToBelow(bool value) => base.WithClipToBelow(value) as LayerBuilder;
+        
         public new LayerBuilder WithMask(Action<MaskBuilder> mask) => base.WithMask(mask) as LayerBuilder;
         
         public new LayerBuilder WithGuid(Guid guid) => base.WithGuid(guid) as LayerBuilder;
@@ -204,6 +214,8 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         
         public new FolderBuilder WithGuid(Guid guid) => base.WithGuid(guid) as FolderBuilder;
 
+        public FolderBuilder WithClipToBelow(bool value) => base.WithClipToBelow(value) as FolderBuilder;
+
         public FolderBuilder WithChildren(Action<ChildrenBuilder> children)
         {
             ChildrenBuilder childrenBuilder = new();
@@ -228,7 +240,7 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         {
             if(buffer.IsEmpty) return this;
             
-            Surface.DrawingSurface.Canvas.DrawBitmap(Bitmap.Decode(buffer), 0, 0);
+            Surface.DrawingSurface.Canvas.DrawBitmap(Bitmap.Decode(buffer), x, y);
             return this;
         }
     }

+ 7 - 4
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -10,8 +10,7 @@ internal static class PixiParserDocumentEx
     {
         return DocumentViewModel.Build(b =>
         {
-            b
-                .WithSize(document.Width, document.Height)
+            b.WithSize(document.Width, document.Height)
                 .WithPalette(document.Palette, x => new Color(x.R, x.G, x.B, x.A))
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A));
 
@@ -43,6 +42,7 @@ internal static class PixiParserDocumentEx
             .WithOpacity(folder.Opacity)
             .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)folder.BlendMode)
             .WithChildren(x => BuildChildren(x, folder.Children))
+            .WithClipToBelow(folder.ClipToMemberBelow)
             .WithMask(folder.Mask, (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height, x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
 
         void BuildLayer(DocumentViewModelBuilder.LayerBuilder builder, ImageLayer layer)
@@ -52,13 +52,16 @@ internal static class PixiParserDocumentEx
                 .WithVisibility(layer.Enabled)
                 .WithOpacity(layer.Opacity)
                 .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)layer.BlendMode)
-                .WithSize(layer.Width, layer.Height)
+                .WithRect(layer.Width, layer.Height, layer.OffsetX, layer.OffsetY)
+                .WithClipToBelow(layer.ClipToMemberBelow)
                 .WithMask(layer.Mask,
                     (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
                         x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
 
             if (layer.Width > 0 && layer.Height > 0)
-                builder.WithSurface(x => x.WithImage(layer.ImageBytes, layer.OffsetX, layer.OffsetY));
+            {
+                builder.WithSurface(x => x.WithImage(layer.ImageBytes, 0, 0));
+            }
         }
     }
 }

+ 26 - 0
src/PixiEditor/Helpers/FocusHelper.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace PixiEditor.Helpers;
+
+internal static class FocusHelper
+{
+    public static void MoveFocusToParent(FrameworkElement element)
+    {
+        FrameworkElement parent = (FrameworkElement)VisualTreeHelper.GetParent(element);
+
+        while (parent is IInputElement elem && !elem.Focusable)
+        {
+            parent = (FrameworkElement)VisualTreeHelper.GetParent(parent);
+        }
+
+        DependencyObject scope = FocusManager.GetFocusScope(element);
+        FocusManager.SetFocusedElement(scope, parent);
+    }
+}

BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png


BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180DegLayers.png


BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270Deg.png


BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270DegLayers.png


BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90Deg.png


BIN=BIN
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90DegLayers.png


+ 12 - 6
src/PixiEditor/Models/Commands/CommandController.cs

@@ -115,16 +115,22 @@ internal class CommandController
         LoadCommands(serviceProvider, compiledCommandList, commandGroupsData, commands, template);
         LoadTools(serviceProvider, commandGroupsData, commands, template);
 
+        var miscList = new List<Command>();
+
         foreach (var (groupInternalName, storedCommands) in commands)
         {
             var groupData = commandGroupsData.FirstOrDefault(group => group.internalName == groupInternalName);
-            string groupDisplayName;
-            if (groupData == default)
-                groupDisplayName = "Misc";
-            else
-                groupDisplayName = groupData.displayName;
-            CommandGroups.Add(new(groupDisplayName, storedCommands));
+            if (groupData == default || groupData.internalName == "PixiEditor.Links")
+            {
+                miscList.AddRange(storedCommands);
+                continue;
+            }
+
+            string groupDisplayName = groupData.displayName;
+            CommandGroups.Add(new CommandGroup(groupDisplayName, storedCommands));
         }
+        
+        CommandGroups.Add(new CommandGroup("Misc", miscList));
     }
 
     private void LoadTools(IServiceProvider serviceProvider, List<(string internalName, string displayName)> commandGroupsData, OneToManyDictionary<string, Command> commands,

+ 34 - 23
src/PixiEditor/Models/Commands/Evaluators/IconEvaluator.cs

@@ -14,39 +14,50 @@ internal class IconEvaluator : Evaluator<ImageSource>
     public override ImageSource CallEvaluate(Command command, object parameter) =>
         base.CallEvaluate(command, parameter ?? command);
 
-    [DebuggerDisplay("IconEvaluator.Default")]
-    private class CommandNameEvaluator : IconEvaluator
+    public static string GetDefaultPath(Command command)
     {
-        public static string[] resources = GetResourceNames();
-
-        public static Dictionary<string, BitmapImage> images = new();
+        string path;
 
-        public override ImageSource CallEvaluate(Command command, object parameter)
+        if (command.IconPath != null)
         {
-            string path;
-
-            if (command.IconPath != null)
+            if (command.IconPath.StartsWith('@'))
             {
-                if (command.IconPath.StartsWith('@'))
-                {
-                    path = command.IconPath[1..];
-                }
-                else
-                {
-                    path = $"Images/{command.IconPath}";
-                }
+                path = command.IconPath[1..];
+            }
+            else if (command.IconPath.StartsWith('$'))
+            {
+                path = $"Images/Commands/{command.IconPath[1..].Replace('.', '/')}.png";
             }
             else
             {
-                path = $"Images/Commands/{command.InternalName.Replace('.', '/')}.png";
+                path = $"Images/{command.IconPath}";
             }
+        }
+        else
+        {
+            path = $"Images/Commands/{command.InternalName.Replace('.', '/')}.png";
+        }
 
-            path = path.ToLower();
+        path = path.ToLower();
 
-            if (path.StartsWith("/"))
-            {
-                path = path[1..];
-            }
+        if (path.StartsWith("/"))
+        {
+            path = path[1..];
+        }
+
+        return path;
+    }
+
+    [DebuggerDisplay("IconEvaluator.Default")]
+    private class CommandNameEvaluator : IconEvaluator
+    {
+        public static string[] resources = GetResourceNames();
+
+        public static Dictionary<string, BitmapImage> images = new();
+
+        public override ImageSource CallEvaluate(Command command, object parameter)
+        {
+            string path = GetDefaultPath(command);
 
             if (resources.Contains(path))
             {

+ 38 - 12
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -7,6 +7,7 @@ using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Helpers;
@@ -68,15 +69,31 @@ internal static class ClipboardController
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static bool TryPasteFromClipboard(DocumentViewModel document)
+    public static bool TryPaste(DocumentViewModel document, DataObject data, bool pasteAsNew = false)
     {
-        List<(string? name, Surface image)> images = GetImagesFromClipboard();
+        List<(string? name, Surface image)> images = GetImage(data);
         if (images.Count == 0)
             return false;
 
         if (images.Count == 1)
         {
-            document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
+            if (pasteAsNew)
+            {
+                var guid = document.Operations.CreateStructureMember(StructureMemberType.Layer, "New Layer", false);
+
+                if (guid == null)
+                {
+                    return false;
+                }
+                
+                document.Operations.SetSelectedMember(guid.Value);
+                document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero, guid.Value, false);
+            }
+            else
+            {
+                document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
+            }
+            
             return true;
         }
 
@@ -84,12 +101,19 @@ internal static class ClipboardController
         return true;
     }
 
+    /// <summary>
+    ///     Pastes image from clipboard into new layer.
+    /// </summary>
+    public static bool TryPasteFromClipboard(DocumentViewModel document, bool pasteAsNew = false) =>
+        TryPaste(document, ClipboardHelper.TryGetDataObject(), pasteAsNew);
+
+    public static List<(string? name, Surface image)> GetImagesFromClipboard() => GetImage(ClipboardHelper.TryGetDataObject());
+
     /// <summary>
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
-    private static List<(string? name, Surface image)> GetImagesFromClipboard()
+    public static List<(string? name, Surface image)> GetImage(DataObject? data)
     {
-        DataObject data = ClipboardHelper.TryGetDataObject();
         List<(string? name, Surface image)> surfaces = new();
 
         if (data == null)
@@ -121,15 +145,16 @@ internal static class ClipboardController
         return surfaces;
     }
 
-    public static bool IsImageInClipboard()
+    public static bool IsImageInClipboard() => IsImage(ClipboardHelper.TryGetDataObject());
+    
+    public static bool IsImage(DataObject? dataObject)
     {
-        DataObject dao = ClipboardHelper.TryGetDataObject();
-        if (dao == null)
+        if (dataObject == null)
             return false;
 
         try
         {
-            var files = dao.GetFileDropList();
+            var files = dataObject.GetFileDropList();
             if (files != null)
             {
                 foreach (var file in files)
@@ -146,8 +171,7 @@ internal static class ClipboardController
             return false;
         }
 
-        return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
-               dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop);
+        return HasData(dataObject, "PNG", DataFormats.Dib, DataFormats.Bitmap);
     }
 
     private static BitmapSource FromPNG(DataObject data)
@@ -158,6 +182,8 @@ internal static class ClipboardController
         return decoder.Frames[0];
     }
 
+    private static bool HasData(DataObject dataObject, params string[] formats) => formats.Any(dataObject.GetDataPresent);
+    
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
         try
@@ -168,7 +194,7 @@ internal static class ClipboardController
             {
                 source = FromPNG(data);
             }
-            else if (data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap))
+            else if (HasData(data, DataFormats.Dib, DataFormats.Bitmap))
             {
                 source = Clipboard.GetImage();
             }

+ 2 - 0
src/PixiEditor/Models/Dialogs/ExportFileDialog.cs

@@ -78,11 +78,13 @@ internal class ExportFileDialog : CustomDialog
     {
         ExportFilePopup popup = new ExportFilePopup(FileWidth, FileHeight);
         popup.ShowDialog();
+
         if (popup.DialogResult == true)
         {
             FileWidth = popup.SaveWidth;
             FileHeight = popup.SaveHeight;
             FilePath = popup.SavePath;
+
             ChosenFormat = popup.SaveFormat;
         }
 

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

@@ -15,24 +15,22 @@ namespace PixiEditor.Models.DocumentModels;
 #nullable enable
 internal class ActionAccumulator
 {
-    private const long minMsPerUpdate = 1000 / 60;
-    private Stopwatch updateStopwatch = Stopwatch.StartNew();
-    private long lastUpdateMs = 0;
-
     private bool executing = false;
 
     private List<IAction> queuedActions = new();
     private DocumentViewModel document;
     private DocumentInternalParts internals;
 
-    private WriteableBitmapUpdater renderer;
+    private CanvasUpdater canvasUpdater;
+    private MemberPreviewUpdater previewUpdater;
 
     public ActionAccumulator(DocumentViewModel doc, DocumentInternalParts internals)
     {
         this.document = doc;
         this.internals = internals;
 
-        renderer = new(doc, internals);
+        canvasUpdater = new(doc, internals);
+        previewUpdater = new(doc, internals);
     }
 
     public void AddFinishedActions(params IAction[] actions)
@@ -63,13 +61,6 @@ internal class ActionAccumulator
 
         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
             var toExecute = queuedActions;
             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
 
             // update the contents of the bitmaps
-            var affectedChunks = new AffectedChunkGatherer(internals.Tracker, optimizedChanges);
-            var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, undoBoundaryPassed);
-            
+            var affectedAreas = new AffectedAreasGatherer(internals.Tracker, optimizedChanges);
+            List<IRenderInfo> renderResult = new();
+            renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+            renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
+
             // lock bitmaps
             foreach (var (_, bitmap) in document.LazyBitmaps)
             {

+ 8 - 0
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -89,6 +89,9 @@ internal class ChangeExecutionController
         return true;
     }
 
+    public void MidChangeUndoInlet() => currentSession?.OnMidChangeUndo();
+    public void MidChangeRedoInlet() => currentSession?.OnMidChangeRedo();
+
     public void ConvertedKeyDownInlet(Key key)
     {
         currentSession?.OnConvertedKeyDown(key);
@@ -175,4 +178,9 @@ internal class ChangeExecutionController
     {
         currentSession?.OnLineOverlayMoved(start, end);
     }
+
+    public void SelectedObjectNudgedInlet(VecI distance)
+    {
+        currentSession?.OnSelectedObjectNudged(distance);
+    }
 }

+ 10 - 6
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -378,14 +378,18 @@ internal class DocumentUpdater
                 ProcessCreateStructureMember(childInfo);
             }
         }
-
-        if (doc.SelectedStructureMember is null)
+        
+        if (doc.SelectedStructureMember is not null)
         {
-            doc.InternalSetSelectedMember(memberVM);
-            memberVM.Selection = StructureMemberSelectionType.Hard;
-            doc.RaisePropertyChanged(nameof(doc.SelectedStructureMember));
-            doc.RaisePropertyChanged(nameof(memberVM.Selection));
+            doc.SelectedStructureMember.Selection = StructureMemberSelectionType.None;
+            doc.SelectedStructureMember.RaisePropertyChanged(nameof(doc.SelectedStructureMember.Selection));
         }
+        
+        doc.InternalSetSelectedMember(memberVM);
+        memberVM.Selection = StructureMemberSelectionType.Hard;
+        doc.RaisePropertyChanged(nameof(doc.SelectedStructureMember));
+        doc.RaisePropertyChanged(nameof(memberVM.Selection));
+
         doc.InternalRaiseLayersChanged(new LayersChangedEventArgs(info.GuidValue, LayerAction.Add));
     }
 

+ 182 - 3
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -24,6 +24,9 @@ internal class DocumentOperationsModule
         Internals = internals;
     }
 
+    /// <summary>
+    /// Creates a new selection with the size of the document
+    /// </summary>
     public void SelectAll()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -33,6 +36,9 @@ internal class DocumentOperationsModule
             new EndSelectRectangle_Action());
     }
 
+    /// <summary>
+    /// Clears the current selection
+    /// </summary>
     public void ClearSelection()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -40,6 +46,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
     }
 
+    /// <summary>
+    /// Deletes selected pixels
+    /// </summary>
+    /// <param name="clearSelection">Should the selection be cleared</param>
     public void DeleteSelectedPixels(bool clearSelection = false)
     {
         var member = Document.SelectedStructureMember;
@@ -54,6 +64,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions();
     }
 
+    /// <summary>
+    /// Sets the opacity of the member with the guid <paramref name="memberGuid"/>
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member</param>
+    /// <param name="value">A value between 0 and 1</param>
     public void SetMemberOpacity(Guid memberGuid, float value)
     {
         if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
@@ -63,10 +78,20 @@ internal class DocumentOperationsModule
             new EndStructureMemberOpacity_Action());
     }
 
+    /// <summary>
+    /// Adds a new viewport or updates a existing one
+    /// </summary>
     public void AddOrUpdateViewport(ViewportInfo info) => Internals.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(info));
 
+    /// <summary>
+    /// Deletes the viewport with the <paramref name="viewportGuid"/>
+    /// </summary>
+    /// <param name="viewportGuid">The Guid of the viewport to remove</param>
     public void RemoveViewport(Guid viewportGuid) => Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
 
+    /// <summary>
+    /// Delete the whole undo stack
+    /// </summary>
     public void ClearUndo()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -74,6 +99,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
     }
 
+    /// <summary>
+    /// Pastes the <paramref name="images"/> as new layers
+    /// </summary>
+    /// <param name="images">The images to paste</param>
     public void PasteImagesAsLayers(List<(string? name, Surface image)> images)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -90,19 +119,29 @@ internal class DocumentOperationsModule
 
         foreach (var imageWithName in images)
         {
-            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, imageWithName.name, true);
+            var layerGuid = Internals.StructureHelper.CreateNewStructureMember(StructureMemberType.Layer, imageWithName.name);
             DrawImage(imageWithName.image, new ShapeCorners(new RectD(VecD.Zero, imageWithName.image.Size)), layerGuid, true, false, false);
         }
         Internals.ActionAccumulator.AddFinishedActions();
     }
 
-    public Guid? CreateStructureMember(StructureMemberType type, string? name = null)
+    /// <summary>
+    /// Creates a new structure member of type <paramref name="type"/> with the name <paramref name="name"/>
+    /// </summary>
+    /// <param name="type">The type of the member</param>
+    /// <param name="name">The name of the member</param>
+    /// <returns>The Guid of the new structure member or null if there is already an active change</returns>
+    public Guid? CreateStructureMember(StructureMemberType type, string? name = null, bool finish = true)
     {
         if (Internals.ChangeController.IsChangeActive)
             return null;
-        return Internals.StructureHelper.CreateNewStructureMember(type, name, true);
+        return Internals.StructureHelper.CreateNewStructureMember(type, name, finish);
     }
 
+    /// <summary>
+    /// Duplicates the layer with the <paramref name="guidValue"/>
+    /// </summary>
+    /// <param name="guidValue">The Guid of the layer</param>
     public void DuplicateLayer(Guid guidValue)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -110,6 +149,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
     }
 
+    /// <summary>
+    /// Delete the member with the <paramref name="guidValue"/>
+    /// </summary>
+    /// <param name="guidValue">The Guid of the layer</param>
     public void DeleteStructureMember(Guid guidValue)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -117,6 +160,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMember_Action(guidValue));
     }
 
+    /// <summary>
+    /// Deletes all members with the <paramref name="guids"/>
+    /// </summary>
+    /// <param name="guids">The Guids of the layers to delete</param>
     public void DeleteStructureMembers(IReadOnlyList<Guid> guids)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -124,6 +171,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(guids.Select(static guid => new DeleteStructureMember_Action(guid)).ToArray());
     }
 
+    /// <summary>
+    /// Resizes the canvas (Does not upscale the content of the image)
+    /// </summary>
+    /// <param name="newSize">The size the canvas should be resized to</param>
+    /// <param name="anchor">Where the existing content should be put</param>
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
@@ -146,6 +198,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ResizeCanvas_Action(newSize, anchor));
     }
 
+    /// <summary>
+    /// Resizes the image (Upscales the content of the image)
+    /// </summary>
+    /// <param name="newSize">The size the image should be resized to</param>
+    /// <param name="resampling">The resampling method to use</param>
     public void ResizeImage(VecI newSize, ResamplingMethod resampling)
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
@@ -168,6 +225,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(newSize, resampling));
     }
 
+    /// <summary>
+    /// Replaces all <paramref name="oldColor"/> with <paramref name="newColor"/>
+    /// </summary>
+    /// <param name="oldColor">The color to replace</param>
+    /// <param name="newColor">The new color</param>
     public void ReplaceColor(Color oldColor, Color newColor)
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
@@ -175,6 +237,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
     }
 
+    /// <summary>
+    /// Creates a new mask on the <paramref name="member"/>
+    /// </summary>
     public void CreateMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -184,6 +249,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CreateStructureMemberMask_Action(member.GuidValue));
     }
 
+    /// <summary>
+    /// Deletes the mask of the <paramref name="member"/>
+    /// </summary>
     public void DeleteMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -191,6 +259,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
     }
     
+    /// <summary>
+    /// Applies the mask to the image
+    /// </summary>
     public void ApplyMask(StructureMemberViewModel member)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -199,28 +270,69 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
     }
 
+    /// <summary>
+    /// Sets the selected structure memeber
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to select</param>
     public void SetSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Adds a member to the soft selection
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to add</param>
     public void AddSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Removes a member from the soft selection
+    /// </summary>
+    /// <param name="memberGuid">The Guid of the member to remove</param>
     public void RemoveSoftSelectedMember(Guid memberGuid) => Internals.ActionAccumulator.AddActions(new RemoveSoftSelectedMember_PassthroughAction(memberGuid));
 
+    /// <summary>
+    /// Clears the soft selection
+    /// </summary>
     public void ClearSoftSelectedMembers() => Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
 
+    /// <summary>
+    /// Undo last change
+    /// </summary>
     public void Undo()
     {
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeUndoInlet();
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
 
+    /// <summary>
+    /// Redo previously undone change
+    /// </summary>
     public void Redo()
     {
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeRedoInlet();
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 
+    public void NudgeSelectedObject(VecI distance)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.SelectedObjectNudgedInlet(distance);
+        }    
+    }
+
+    /// <summary>
+    /// Moves a member next to or inside another structure member
+    /// </summary>
+    /// <param name="memberToMove">The member to move</param>
+    /// <param name="memberToMoveIntoOrNextTo">The target member</param>
+    /// <param name="placement">Where to place the <paramref name="memberToMove"/></param>
     public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
     {
         if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
@@ -228,6 +340,9 @@ internal class DocumentOperationsModule
         Internals.StructureHelper.TryMoveStructureMember(memberToMove, memberToMoveIntoOrNextTo, placement);
     }
 
+    /// <summary>
+    /// Merge all structure members with the Guids inside <paramref name="members"/>
+    /// </summary>
     public void MergeStructureMembers(IReadOnlyList<Guid> members)
     {
         if (Internals.ChangeController.IsChangeActive || members.Count < 2)
@@ -248,6 +363,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
     }
 
+    /// <summary>
+    /// Starts a image transform and pastes the transformed image on the currently selected layer
+    /// </summary>
+    /// <param name="image">The image to paste</param>
+    /// <param name="startPos">Where the transform should start</param>
     public void PasteImageWithTransform(Surface image, VecI startPos)
     {
         if (Document.SelectedStructureMember is null)
@@ -255,6 +375,20 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos));
     }
 
+    /// <summary>
+    /// Starts a image transform and pastes the transformed image on the currently selected layer
+    /// </summary>
+    /// <param name="image">The image to paste</param>
+    /// <param name="startPos">Where the transform should start</param>
+    public void PasteImageWithTransform(Surface image, VecI startPos, Guid memberGuid, bool drawOnMask)
+    {
+        Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos, memberGuid, drawOnMask));
+    }
+
+    /// <summary>
+    /// Starts a transform on the selected area
+    /// </summary>
+    /// <param name="toolLinked">Is this transform started by a tool</param>
     public void TransformSelectedArea(bool toolLinked)
     {
         if (Document.SelectedStructureMember is null ||
@@ -264,6 +398,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
     }
 
+    /// <summary>
+    /// Ties stopping the currently executing tool linked executor
+    /// </summary>
     public void TryStopToolLinkedExecutor()
     {
         if (Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked)
@@ -273,6 +410,15 @@ internal class DocumentOperationsModule
     public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
         DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
 
+    /// <summary>
+    /// Draws a image on the member with the <paramref name="memberGuid"/>
+    /// </summary>
+    /// <param name="image">The image to draw onto the layer</param>
+    /// <param name="corners">The shape the image should fit into</param>
+    /// <param name="memberGuid">The Guid of the member to paste on</param>
+    /// <param name="ignoreClipSymmetriesEtc">Ignore selection clipping and symmetry (See DrawingChangeHelper.ApplyClipsSymmetriesEtc of UpdateableDocument)</param>
+    /// <param name="drawOnMask">Draw on the mask or on the image</param>
+    /// <param name="finish">Is this a finished action</param>
     private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -284,6 +430,9 @@ internal class DocumentOperationsModule
             Internals.ActionAccumulator.AddFinishedActions();
     }
 
+    /// <summary>
+    /// Resizes the canvas to fit the content
+    /// </summary>
     public void ClipCanvas()
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -291,8 +440,14 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
     }
 
+    /// <summary>
+    /// Flips the image on the <paramref name="flipType"/> axis
+    /// </summary>
     public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
 
+    /// <summary>
+    /// Flips the members with the Guids of <paramref name="membersToFlip"/> on the <paramref name="flipType"/> axis
+    /// </summary>
     public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -301,8 +456,16 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
     }
 
+    /// <summary>
+    /// Rotates the image
+    /// </summary>
+    /// <param name="rotation">The degrees to rotate the image by</param>
     public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
 
+    /// <summary>
+    /// Rotates the members with the Guids of <paramref name="membersToRotate"/>
+    /// </summary>
+    /// <param name="rotation">The degrees to rotate the members by</param>
     public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -311,6 +474,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
     }
     
+    /// <summary>
+    /// Puts the content of the image in the middle of the canvas
+    /// </summary>
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -319,6 +485,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList()));
     }
 
+    /// <summary>
+    /// Imports a reference layer from a Pbgra Int32 array
+    /// </summary>
+    /// <param name="imageSize">The size of the image</param>
     public void ImportReferenceLayer(ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize)
     {
         if (Internals.ChangeController.IsChangeActive)
@@ -329,6 +499,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imagePbgra32Bytes, imageSize));
     }
 
+    /// <summary>
+    /// Deletes the reference layer
+    /// </summary>
     public void DeleteReferenceLayer()
     {
         if (Internals.ChangeController.IsChangeActive || Document.ReferenceLayerViewModel.ReferenceBitmap is null)
@@ -337,6 +510,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteReferenceLayer_Action());
     }
 
+    /// <summary>
+    /// Starts a transform on the reference layer
+    /// </summary>
     public void TransformReferenceLayer()
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
@@ -344,6 +520,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
     }
 
+    /// <summary>
+    /// Resets the reference layer transform
+    /// </summary>
     public void ResetReferenceLayerPosition()
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LassoToolExecutor.cs

@@ -12,7 +12,7 @@ internal sealed class LassoToolExecutor : UpdateableChangeExecutor
     
     public override ExecutionState Start()
     {
-        mode = ViewModelMain.Current?.ToolsSubViewModel.GetTool<LassoToolViewModel>()?.SelectMode;
+        mode = ViewModelMain.Current?.ToolsSubViewModel.GetTool<LassoToolViewModel>()?.ResultingSelectionMode;
 
         if (mode is null)
             return ExecutionState.Error;

+ 29 - 10
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs

@@ -55,11 +55,11 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         if (transforming)
             return;
         started = true;
-        curPos = pos;
-        VecI targetPos = pos;
+
         if (toolViewModel!.Snap)
-            targetPos = ShapeToolExecutor<ShapeTool>.Get45IncrementedPosition(startPos, curPos);
-        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, targetPos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
+            pos = ShapeToolExecutor<ShapeTool>.Get45IncrementedPosition(startPos, pos);
+        curPos = pos;
+        internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, startPos, pos, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
     }
 
     public override void OnLeftMouseButtonUp()
@@ -69,10 +69,8 @@ internal class LineToolExecutor : UpdateableChangeExecutor
             onEnded!(this);
             return;
         }
-        
-        document!.LineToolOverlayViewModel.LineStart = startPos + new VecD(0.5);
-        document!.LineToolOverlayViewModel.LineEnd = curPos + new VecD(0.5);
-        document!.LineToolOverlayViewModel.IsEnabled = true;
+
+        document!.LineToolOverlayViewModel.Show(startPos + new VecD(0.5), curPos + new VecD(0.5));
         transforming = true;
     }
 
@@ -83,12 +81,33 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new DrawLine_Action(memberGuid, (VecI)start, (VecI)end, strokeWidth, strokeColor, StrokeCap.Butt, drawOnMask));
     }
 
+    public override void OnSelectedObjectNudged(VecI distance)
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayViewModel.Nudge(distance);
+    }
+
+    public override void OnMidChangeUndo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayViewModel.Undo();
+    }
+
+    public override void OnMidChangeRedo()
+    {
+        if (!transforming)
+            return;
+        document!.LineToolOverlayViewModel.Redo();
+    }
+
     public override void OnTransformApplied()
     {
         if (!transforming)
             return;
 
-        document!.LineToolOverlayViewModel.IsEnabled = false;
+        document!.LineToolOverlayViewModel.Hide();
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         onEnded!(this);
     }
@@ -96,7 +115,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
     public override void ForceStop()
     {
         if (transforming)
-            document!.LineToolOverlayViewModel.IsEnabled = false;
+            document!.LineToolOverlayViewModel.Hide();
 
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
     }

+ 34 - 14
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -11,7 +11,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
     private readonly Surface image;
     private readonly VecI pos;
     private bool drawOnMask;
-    private Guid memberGuid;
+    private Guid? memberGuid;
 
     public PasteImageExecutor(Surface image, VecI pos)
     {
@@ -19,32 +19,52 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         this.pos = pos;
     }
 
+    public PasteImageExecutor(Surface image, VecI pos, Guid memberGuid, bool drawOnMask)
+    {
+        this.image = image;
+        this.pos = pos;
+        this.memberGuid = memberGuid;
+        this.drawOnMask = drawOnMask;
+    }
+    
     public override ExecutionState Start()
     {
-        var member = document!.SelectedStructureMember;
+        if (memberGuid == null)
+        {
+            var member = document!.SelectedStructureMember;
 
-        if (member is null)
-            return ExecutionState.Error;
-        drawOnMask = member is LayerViewModel layer ? layer.ShouldDrawOnMask : true;
-        if (drawOnMask && !member.HasMaskBindable)
-            return ExecutionState.Error;
-        if (!drawOnMask && member is not LayerViewModel)
-            return ExecutionState.Error;
-
-        memberGuid = member.GuidValue;
+            if (member is null)
+                return ExecutionState.Error;
+            drawOnMask = member is not LayerViewModel layer || layer.ShouldDrawOnMask;
+            
+            switch (drawOnMask)
+            {
+                case true when !member.HasMaskBindable:
+                case false when member is not LayerViewModel:
+                    return ExecutionState.Error;
+            }
+            
+            memberGuid = member.GuidValue;
+        }
 
         ShapeCorners corners = new(new RectD(pos, image.Size));
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
-        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners);
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, true);
 
         return ExecutionState.Success;
     }
 
     public override void OnTransformMoved(ShapeCorners corners)
     {
-        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid, false, drawOnMask));
+        internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask));
     }
 
+    public override void OnSelectedObjectNudged(VecI distance) => document!.TransformViewModel.Nudge(distance);
+
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndPasteImage_Action());

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SelectToolExecutor.cs

@@ -31,7 +31,7 @@ internal class SelectToolExecutor : UpdateableChangeExecutor
         
         startPos = controller!.LastPixelPosition;
         selectShape = toolViewModel.SelectShape;
-        selectMode = toolViewModel.SelectMode;
+        selectMode = toolViewModel.ResultingSelectionMode;
 
         IAction action = CreateUpdateAction(selectShape, new RectI(startPos, new(0)), selectMode);
         internals!.ActionAccumulator.AddActions(action);

+ 22 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShapeToolExecutor.cs

@@ -114,6 +114,27 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
         onEnded?.Invoke(this);
     }
 
+    public override void OnSelectedObjectNudged(VecI distance)
+    {
+        if (!transforming)
+            return;
+        document!.TransformViewModel.Nudge(distance);
+    }
+
+    public override void OnMidChangeUndo()
+    {
+        if (!transforming)
+            return;
+        document!.TransformViewModel.Undo();
+    }
+
+    public override void OnMidChangeRedo()
+    {
+        if (!transforming)
+            return;
+        document!.TransformViewModel.Redo();
+    }
+
     public override void OnPixelPositionChange(VecI pos)
     {
         if (transforming)
@@ -133,7 +154,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
             return;
         }
         transforming = true;
-        document!.TransformViewModel.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect));
+        document!.TransformViewModel.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), true);
     }
 
     public override void ForceStop()

+ 8 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Enums;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -15,7 +16,7 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor
             return ExecutionState.Error;
 
         ShapeCorners corners = document.ReferenceLayerViewModel.ReferenceShapeBindable;
-        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, true, corners);
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, true, corners, true);
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
         return ExecutionState.Success;
     }
@@ -25,6 +26,12 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
     }
 
+    public override void OnSelectedObjectNudged(VecI distance) => document!.TransformViewModel.Nudge(distance);
+
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndTransformReferenceLayer_Action());

+ 11 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs

@@ -1,4 +1,5 @@
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
@@ -32,7 +33,7 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             return ExecutionState.Error;
 
         ShapeCorners corners = new(document.SelectionPathBindable.TightBounds);
-        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners);
+        document.TransformViewModel.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, Type == ExecutorType.Regular);
         membersToTransform = members.Select(static a => a.GuidValue).ToArray();
         internals!.ActionAccumulator.AddActions(
             new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false));
@@ -45,8 +46,17 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false));
     }
 
+    public override void OnSelectedObjectNudged(VecI distance) => document!.TransformViewModel.Nudge(distance);
+
+    public override void OnMidChangeUndo() => document!.TransformViewModel.Undo();
+
+    public override void OnMidChangeRedo() => document!.TransformViewModel.Redo();
+
     public override void OnTransformApplied()
     {
+        if (Type == ExecutorType.ToolLinked)
+            return;
+
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformViewModel.HideTransform();

+ 3 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -47,4 +47,7 @@ internal abstract class UpdateableChangeExecutor
     public virtual void OnTransformMoved(ShapeCorners corners) { }
     public virtual void OnTransformApplied() { }
     public virtual void OnLineOverlayMoved(VecD start, VecD end) { }
+    public virtual void OnMidChangeUndo() { }
+    public virtual void OnMidChangeRedo() { }
+    public virtual void OnSelectedObjectNudged(VecI distance) { }
 }

+ 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 PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -11,15 +12,15 @@ using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.Models.Rendering;
 #nullable enable
-internal class AffectedChunkGatherer
+internal class AffectedAreasGatherer
 {
     private readonly DocumentChangeTracker tracker;
 
-    public HashSet<VecI> MainImageChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> ImagePreviewChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> MaskPreviewChunks { get; private set; } = new();
+    public AffectedArea MainImageArea { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
 
-    public AffectedChunkGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
+    public AffectedAreasGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
     {
         this.tracker = tracker;
         ProcessChanges(changes);
@@ -31,18 +32,18 @@ internal class AffectedChunkGatherer
         {
             switch (change)
             {
-                case MaskChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case MaskArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks, true);
-                    AddToMaskPreview(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area, true);
+                    AddToMaskPreview(info.GuidValue, info.Area);
                     break;
-                case LayerImageChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case LayerImageArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area);
                     break;
                 case CreateStructureMember_ChangeInfo info:
                     AddAllToMainImage(info.GuidValue);
@@ -99,7 +100,7 @@ internal class AffectedChunkGatherer
         if (member is IReadOnlyLayer layer)
         {
             var chunks = layer.LayerImage.FindAllChunks();
-            AddToImagePreviews(memberGuid, chunks, ignoreSelf);
+            AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         else if (member is IReadOnlyFolder folder)
         {
@@ -117,7 +118,7 @@ internal class AffectedChunkGatherer
             var chunks = layer.LayerImage.FindAllChunks();
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
-            AddToMainImage(chunks);
+            AddToMainImage(new AffectedArea(chunks));
         }
         else
         {
@@ -132,7 +133,7 @@ internal class AffectedChunkGatherer
         if (member.Mask is not null)
         {
             var chunks = member.Mask.FindAllChunks();
-            AddToMaskPreview(memberGuid, chunks);
+            AddToMaskPreview(memberGuid, new AffectedArea(chunks));
         }
         if (member is IReadOnlyFolder folder)
         {
@@ -142,12 +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);
         if (path.Count < 2)
@@ -155,25 +158,37 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
             var member = path[i];
-            if (!ImagePreviewChunks.ContainsKey(member.GuidValue))
-                ImagePreviewChunks[member.GuidValue] = new HashSet<VecI>(chunks);
+            if (!ImagePreviewAreas.ContainsKey(member.GuidValue))
+            {
+                ImagePreviewAreas[member.GuidValue] = new AffectedArea(area);
+            }
             else
-                ImagePreviewChunks[member.GuidValue].UnionWith(chunks);
+            {
+                var temp = ImagePreviewAreas[member.GuidValue];
+                temp.UnionWith(area);
+                ImagePreviewAreas[member.GuidValue] = temp;
+            }
         }
     }
 
-    private void AddToMaskPreview(Guid memberGuid, HashSet<VecI> chunks)
+    private void AddToMaskPreview(Guid memberGuid, AffectedArea area)
     {
-        if (!MaskPreviewChunks.ContainsKey(memberGuid))
-            MaskPreviewChunks[memberGuid] = new HashSet<VecI>(chunks);
+        if (!MaskPreviewAreas.ContainsKey(memberGuid))
+        {
+            MaskPreviewAreas[memberGuid] = new AffectedArea(area);
+        }
         else
-            MaskPreviewChunks[memberGuid].UnionWith(chunks);
+        {
+            var temp = MaskPreviewAreas[memberGuid];
+            temp.UnionWith(area);
+            MaskPreviewAreas[memberGuid] = temp;
+        }
     }
 
 
     private void AddWholeCanvasToMainImage()
     {
-        AddAllChunks(MainImageChunks);
+        MainImageArea = AddWholeArea(MainImageArea);
     }
 
     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++)
         {
             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)
     {
-        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));
     }
 
-    private void AddAllChunks(HashSet<VecI> chunks)
+    private AffectedArea AddWholeArea(AffectedArea area)
     {
         VecI size = new(
             (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++)
             {
-                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);
-            });
-    }
-}

+ 13 - 1
src/PixiEditor/PixiEditor.csproj

@@ -218,7 +218,7 @@
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
 		<PackageReference Include="OneOf" Version="3.0.223" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
-		<PackageReference Include="PixiEditor.Parser" Version="3.0.0" />
+		<PackageReference Include="PixiEditor.Parser" Version="3.1.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="3.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
@@ -356,6 +356,18 @@
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleMask.png" />
 		<None Remove="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate90Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate90Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate180Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate180Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate270Deg.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate270Deg.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate90DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate90DegLayers.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate180DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate180DegLayers.png" />
+		<None Remove="Images\Commands\PixiEditor\Document\Rotate270DegLayers.png" />
+		<Resource Include="Images\Commands\PixiEditor\Document\Rotate270DegLayers.png" />
 	</ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -50,5 +50,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.1.9.5")]
-[assembly: AssemblyFileVersion("0.1.9.5")]
+[assembly: AssemblyVersion("0.1.9.6")]
+[assembly: AssemblyFileVersion("0.1.9.6")]

+ 11 - 42
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -50,48 +50,20 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     public bool DocumentNotNull() => ActiveDocument != null;
 
     [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument")]
-    public void ClipCanvas()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.ClipCanvas();
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipImageHorizontally()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal);
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipLayersHorizontally()
-    {
-        if (ActiveDocument?.SelectedStructureMember == null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal, ActiveDocument.GetSelectedMembers());
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipImageVertical", "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipImageVertically()
-    {
-        if (ActiveDocument is null)
-            return;
-        
-        ActiveDocument?.Operations.FlipImage(FlipType.Vertical);
-    }
-    
-    [Command.Basic("PixiEditor.Document.FlipLayersVertical", "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
-    public void FlipLayersVertically()
+    public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
+
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type);
+
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayers(FlipType type)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        ActiveDocument?.Operations.FlipImage(FlipType.Vertical, ActiveDocument.GetSelectedMembers());
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers());
     }
     
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
@@ -100,10 +72,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
         "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
-    public void RotateImage(RotationAngle angle)
-    {
-        ActiveDocument?.Operations.RotateImage(angle);
-    }
+    public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
         "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]

+ 5 - 3
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.Serialization.cs

@@ -23,7 +23,7 @@ internal partial class DocumentViewModel
     {
         var root = new Folder();
         
-        IReadOnlyDocument doc = Internals.Tracker.Document;
+        var doc = Internals.Tracker.Document;
 
         AddMembers(doc.StructureRoot.Children, doc, root);
 
@@ -31,7 +31,7 @@ internal partial class DocumentViewModel
         {
             Width = Width, Height = Height,
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
-            RootFolder = root, PreviewImage = PreviewSurface.Snapshot().Encode().AsSpan().ToArray()
+            RootFolder = root, PreviewImage = (MaybeRenderWholeImage().Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray()
         };
 
         return document;
@@ -64,6 +64,7 @@ internal partial class DocumentViewModel
             BlendMode = (BlendMode)(int)folder.BlendMode,
             Enabled = folder.IsVisible,
             Opacity = folder.Opacity,
+            ClipToMemberBelow = folder.ClipToMemberBelow,
             Mask = GetMask(folder.Mask, folder.MaskIsVisible)
         };
     }
@@ -79,7 +80,8 @@ internal partial class DocumentViewModel
         {
             Width = result?.Size.X ?? 0, Height = result?.Size.Y ?? 0, OffsetX = tightBounds?.X ?? 0, OffsetY = tightBounds?.Y ?? 0,
             Enabled = layer.IsVisible, BlendMode = (BlendMode)(int)layer.BlendMode, ImageBytes = bytes,
-            Name = layer.Name, Opacity = layer.Opacity, Mask = GetMask(layer.Mask, layer.MaskIsVisible)
+            ClipToMemberBelow = layer.ClipToMemberBelow, Name = layer.Name, 
+            Opacity = layer.Opacity, Mask = GetMask(layer.Mask, layer.MaskIsVisible)
         };
 
         return serializable;

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

@@ -20,6 +20,7 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
@@ -177,6 +178,10 @@ internal partial class DocumentViewModel : NotifyableObject
         ReferenceLayerViewModel = new(this, Internals);
     }
 
+    /// <summary>
+    /// Creates a new document using the <paramref name="builder"/>
+    /// </summary>
+    /// <returns>The created document</returns>
     public static DocumentViewModel Build(Action<DocumentViewModelBuilder> builder)
     {
         var builderInstance = new DocumentViewModelBuilder();
@@ -215,6 +220,10 @@ internal partial class DocumentViewModel : NotifyableObject
 
             if (!member.IsVisible)
                 acc.AddActions(new StructureMemberIsVisible_Action(member.IsVisible, member.GuidValue));
+            
+            acc.AddActions(new StructureMemberBlendMode_Action(member.BlendMode, member.GuidValue));
+            
+            acc.AddActions(new StructureMemberClipToMemberBelow_Action(member.ClipToMemberBelow, member.GuidValue));
 
             if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             {
@@ -278,6 +287,10 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    /// <summary>
+    /// Tries rendering the whole document
+    /// </summary>
+    /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
     public OneOf<Error, Surface> MaybeRenderWholeImage()
     {
         try
@@ -356,6 +369,11 @@ internal partial class DocumentViewModel : NotifyableObject
         return (output, bounds);
     }
 
+    /// <summary>
+    /// Picks the color at <paramref name="pos"/>
+    /// </summary>
+    /// <param name="includeReference">Should the color be picked from the reference layer</param>
+    /// <param name="includeCanvas">Should the color be picked from the canvas</param>
     public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas)
     {
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
@@ -401,7 +419,7 @@ internal partial class DocumentViewModel : NotifyableObject
             if (scope == DocumentScope.AllLayers)
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot)
+                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot, new RectI(pos, VecI.One))
                     .Match<Color>(
                         (Chunk chunk) =>
                         {
@@ -485,6 +503,9 @@ internal partial class DocumentViewModel : NotifyableObject
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
 
+    /// <summary>
+    /// Returns a list of all selected members (Hard and Soft selected)
+    /// </summary>
     public List<Guid> GetSelectedMembers()
     {
         List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };

+ 0 - 41
src/PixiEditor/ViewModels/SubViewModels/Document/LineToolOverlayViewModel.cs

@@ -1,41 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using PixiEditor.DrawingApi.Core.Numerics;
-
-namespace PixiEditor.ViewModels.SubViewModels.Document;
-internal class LineToolOverlayViewModel : NotifyableObject
-{
-    public event EventHandler<(VecD, VecD)> LineMoved;
-
-    private VecD lineStart;
-    public VecD LineStart
-    {
-        get => lineStart;
-        set 
-        {
-            if (SetProperty(ref lineStart, value))
-                LineMoved?.Invoke(this, (lineStart, lineEnd));
-        }
-    }
-
-    private VecD lineEnd;
-    public VecD LineEnd
-    {
-        get => lineEnd;
-        set
-        {
-            if (SetProperty(ref lineEnd, value))
-                LineMoved?.Invoke(this, (lineStart, lineEnd));
-        }
-    }
-
-    private bool isEnabled;
-    public bool IsEnabled
-    {
-        get => isEnabled;
-        set => SetProperty(ref isEnabled, value);
-    }
-}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio