Browse Source

Merge branch 'master' into development

Krzysztof Krysiński 2 years ago
parent
commit
d3775e5447
100 changed files with 1838 additions and 838 deletions
  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
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png
  70. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180DegLayers.png
  71. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270Deg.png
  72. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate270DegLayers.png
  73. BIN
      src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate90Deg.png
  74. 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)
 ![](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
 ### 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.
 PixiEditor started in 2018 and it's been actively developed since. We continuously improve code quality to ensure the best experience and performance.
 
 
 
 
-
 ## Installation
 ## 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>
 <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**
 **Or**
 
 
 Follow these instructions to get PixiEditor working on your machine.
 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
 ## 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
 ### Pixel Art Timelapse - "Bog Landscape" | PixiEditor
 
 
@@ -81,15 +83,15 @@ Struggling with something? You can find support in a few places:
 
 
 ### Software Requirements
 ### Software Requirements
 
 
-* .NET 5
+* .NET 7
 
 
-* Visual Studio
+* latest Visual Studio 2022 (in order to code generators to work)
 
 
 ### Instructions
 ### Instructions
 
 
 1. Clone Repository
 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
 3. Build solution
 
 

+ 1 - 0
src/ChunkyImageLib/Chunk.cs

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

+ 52 - 36
src/ChunkyImageLib/ChunkyImage.cs

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

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

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

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

@@ -55,7 +55,8 @@ public struct ShapeCorners
     {
     {
         get
         get
         {
         {
-            Span<VecD> lengths = stackalloc[] {
+            Span<VecD> lengths = stackalloc[] 
+            {
                 TopLeft - TopRight,
                 TopLeft - TopRight,
                 TopRight - BottomRight,
                 TopRight - BottomRight,
                 BottomRight - BottomLeft,
                 BottomRight - BottomLeft,
@@ -91,6 +92,18 @@ public struct ShapeCorners
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
                 (BottomRight - BottomRight.Round()).TaxicabLength < epsilon;
         }
         }
     }
     }
+    public RectD AABBBounds
+    {
+        get
+        {
+            double minX = Math.Min(Math.Min(TopLeft.X, TopRight.X), Math.Min(BottomLeft.X, BottomRight.X));
+            double minY = Math.Min(Math.Min(TopLeft.Y, TopRight.Y), Math.Min(BottomLeft.Y, BottomRight.Y));
+            double maxX = Math.Max(Math.Max(TopLeft.X, TopRight.X), Math.Max(BottomLeft.X, BottomRight.X));
+            double maxY = Math.Max(Math.Max(TopLeft.Y, TopRight.Y), Math.Max(BottomLeft.Y, BottomRight.Y));
+            return RectD.FromTwoPoints(new VecD(minX, minY), new VecD(maxX, maxY));
+        }
+    }
+
     public bool IsPointInside(VecD point)
     public bool IsPointInside(VecD point)
     {
     {
         var top = TopLeft - TopRight;
         var top = TopLeft - TopRight;
@@ -121,7 +134,7 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectY(horAxisY),
         TopLeft = TopLeft.ReflectY(horAxisY),
         TopRight = TopRight.ReflectY(horAxisY)
         TopRight = TopRight.ReflectY(horAxisY)
     };
     };
-    
+
     public ShapeCorners AsMirroredAcrossVerAxis(int verAxisX) => new ShapeCorners
     public ShapeCorners AsMirroredAcrossVerAxis(int verAxisX) => new ShapeCorners
     {
     {
         BottomLeft = BottomLeft.ReflectX(verAxisX),
         BottomLeft = BottomLeft.ReflectX(verAxisX),
@@ -129,4 +142,39 @@ public struct ShapeCorners
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopLeft = TopLeft.ReflectX(verAxisX),
         TopRight = TopRight.ReflectX(verAxisX)
         TopRight = TopRight.ReflectX(verAxisX)
     };
     };
+
+    public ShapeCorners AsRotated(double angle, VecD around) => new ShapeCorners
+    {
+        BottomLeft = BottomLeft.Rotate(angle, around),
+        BottomRight = BottomRight.Rotate(angle, around),
+        TopLeft = TopLeft.Rotate(angle, around),
+        TopRight = TopRight.Rotate(angle, around)
+    };
+
+    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 GetCommittedPixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
     bool LatestOrCommittedChunkExists(VecI chunkPos);
-    HashSet<VecI> FindAffectedChunks(int fromOperationIndex = 0);
+    AffectedArea FindAffectedArea(int fromOperationIndex = 0);
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindCommittedChunks();
     HashSet<VecI> FindAllChunks();
     HashSet<VecI> FindAllChunks();
 }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 0
src/ChunkyImageLibTest/ChunkyImageLibTest.csproj

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

+ 6 - 14
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -1,30 +1,22 @@
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Skia;
 using Xunit;
 using Xunit;
 
 
 namespace ChunkyImageLibTest;
 namespace ChunkyImageLibTest;
 public class ChunkyImageTests
 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]
     [Fact]

+ 2 - 2
src/ChunkyImageLibTest/ClearRegionOperationTests.cs

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

+ 12 - 1
src/ChunkyImageLibTest/ImageOperationTests.cs

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

+ 14 - 14
src/ChunkyImageLibTest/RectangleOperationTests.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -6,14 +6,13 @@ namespace PixiEditor.ChangeableDocument.Changes.Selection;
 internal class SelectionChangeHelper
 internal class SelectionChangeHelper
 {
 {
     public static Selection_ChangeInfo DoSelectionTransform(
     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);
         VectorPath newPath = new(originalPath);
 
 
         var matrix = Matrix3X3.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y).PostConcat(
         var matrix = Matrix3X3.CreateTranslation((float)-originalPathTightBounds.X, (float)-originalPathTightBounds.Y).PostConcat(
             OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
             OperationHelper.CreateMatrixFromPoints(to, originalPathTightBounds.Size));
         newPath.Transform(matrix);
         newPath.Transform(matrix);
-
         var toDispose = target.Selection.SelectionPath;
         var toDispose = target.Selection.SelectionPath;
         target.Selection.SelectionPath = newPath;
         target.Selection.SelectionPath = newPath;
         toDispose.Dispose();
         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
 internal class TransformSelectionPath_UpdateableChange : UpdateableChange
 {
 {
     private VectorPath? originalPath;
     private VectorPath? originalPath;
-    private RectI originalTightBounds;
+    private RectD originalTightBounds;
     private ShapeCorners newCorners;
     private ShapeCorners newCorners;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
@@ -25,7 +25,7 @@ internal class TransformSelectionPath_UpdateableChange : UpdateableChange
         if (target.Selection.SelectionPath.IsEmpty)
         if (target.Selection.SelectionPath.IsEmpty)
             return false;
             return false;
         originalPath = new(target.Selection.SelectionPath);
         originalPath = new(target.Selection.SelectionPath);
-        originalTightBounds = (RectI)originalPath.TightBounds;
+        originalTightBounds = originalPath.TightBounds;
         return true;
         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)!;
         var layer = (Layer)target.FindMember(structureMemberGuid)!;
         layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
         layer!.LayerImage.EnqueueApplyMask(layer.Mask!);
         ignoreInUndo = false;
         ignoreInUndo = false;
-        var layerInfo = new LayerImageChunks_ChangeInfo(structureMemberGuid, layer.LayerImage.FindAffectedChunks());
-        savedChunks = new CommittedChunkStorage(layer.LayerImage, layerInfo.Chunks);
+        var layerInfo = new LayerImageArea_ChangeInfo(structureMemberGuid, layer.LayerImage.FindAffectedArea());
+        savedChunks = new CommittedChunkStorage(layer.LayerImage, layerInfo.Area.Chunks);
         
         
         layer.LayerImage.CommitChanges();
         layer.LayerImage.CommitChanges();
         return layerInfo;
         return layerInfo;
@@ -40,6 +40,6 @@ internal sealed class ApplyMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
         var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, structureMemberGuid, false, ref savedChunks);
-        return new LayerImageChunks_ChangeInfo(structureMemberGuid, affected);
+        return new LayerImageArea_ChangeInfo(structureMemberGuid, affected);
     }
     }
 }
 }

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

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

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

@@ -5,7 +5,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge
 {
 {
     public static class DrawingBackendApi
     public static class DrawingBackendApi
     {
     {
-        private static IDrawingBackend _current;
+        private static IDrawingBackend? _current;
 
 
         public 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>
     /// <summary>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>
     /// </summary>

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

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 
 
 namespace PixiEditor.DrawingApi.Core.Numerics;
 namespace PixiEditor.DrawingApi.Core.Numerics;
@@ -193,6 +194,50 @@ public struct RectI : IEquatable<RectI>
         };
         };
     }
     }
 
 
+    public readonly RectD Scale(double multiplier)
+    {
+        return new RectD()
+        {
+            Left = left * multiplier,
+            Right = right * multiplier,
+            Top = top * multiplier,
+            Bottom = bottom * multiplier
+        };
+    }
+
+    public readonly RectD Scale(double multiplier, VecD relativeTo)
+    {
+        return new RectD()
+        {
+            Left = (left - relativeTo.X) * multiplier + relativeTo.X,
+            Right = (right - relativeTo.X) * multiplier + relativeTo.X,
+            Top = (top - relativeTo.Y) * multiplier + relativeTo.Y,
+            Bottom = (bottom - relativeTo.Y) * multiplier + relativeTo.Y
+        };
+    }
+
+    public readonly RectI Translate(VecI delta)
+    {
+        return new RectI()
+        {
+            Left = left + delta.X,
+            Right = right + delta.X,
+            Top = top + delta.Y,
+            Bottom = bottom + delta.Y
+        };
+    }
+
+    public readonly RectD Translate(VecD delta)
+    {
+        return new RectD()
+        {
+            Left = left + delta.X,
+            Right = right + delta.X,
+            Top = top + delta.Y,
+            Bottom = bottom + delta.Y
+        };
+    }
+
     /// <summary>
     /// <summary>
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// Fits passed rectangle into this rectangle while maintaining aspect ratio
     /// </summary>
     /// </summary>
@@ -200,7 +245,7 @@ public struct RectI : IEquatable<RectI>
     {
     {
         RectD rectD = (RectD)rect;
         RectD rectD = (RectD)rect;
         RectD thisD = (RectD)this;
         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);
         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;
         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.Controls.Primitives;
 using System.Windows.Input;
 using System.Windows.Input;
 using Microsoft.Xaml.Behaviors;
 using Microsoft.Xaml.Behaviors;
-
 namespace PixiEditor.Helpers.Behaviours;
 namespace PixiEditor.Helpers.Behaviours;
 #nullable enable
 #nullable enable
 internal class SliderUpdateBehavior : Behavior<Slider>
 internal class SliderUpdateBehavior : Behavior<Slider>
@@ -41,6 +40,15 @@ internal class SliderUpdateBehavior : Behavior<Slider>
         set => SetValue(DragStartedProperty, value);
         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 =
     public static DependencyProperty ValueFromSliderProperty =
         DependencyProperty.Register(nameof(ValueFromSlider), typeof(double), typeof(SliderUpdateBehavior), new(OnSliderValuePropertyChange));
         DependencyProperty.Register(nameof(ValueFromSlider), typeof(double), typeof(SliderUpdateBehavior), new(OnSliderValuePropertyChange));
     public double ValueFromSlider
     public double ValueFromSlider
@@ -55,6 +63,8 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private bool bindingValueChangedWhileDragging = false;
     private bool bindingValueChangedWhileDragging = false;
     private double bindingValueWhileDragging = 0.0;
     private double bindingValueWhileDragging = 0.0;
 
 
+    private bool skipSetOpacity;
+    
     protected override void OnAttached()
     protected override void OnAttached()
     {
     {
         AssociatedObject.Loaded += AssociatedObject_Loaded;
         AssociatedObject.Loaded += AssociatedObject_Loaded;
@@ -101,23 +111,32 @@ internal class SliderUpdateBehavior : Behavior<Slider>
     private static void OnSliderValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     private static void OnSliderValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        
         if (obj.dragging)
         if (obj.dragging)
         {
         {
             if (obj.DragValueChanged is not null && obj.DragValueChanged.CanExecute(e.NewValue))
             if (obj.DragValueChanged is not null && obj.DragValueChanged.CanExecute(e.NewValue))
                 obj.DragValueChanged.Execute(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)
     private static void OnBindingValuePropertyChange(DependencyObject slider, DependencyPropertyChangedEventArgs e)
     {
     {
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
         SliderUpdateBehavior obj = (SliderUpdateBehavior)slider;
+        obj.skipSetOpacity = true;
         if (obj.dragging)
         if (obj.dragging)
         {
         {
             obj.bindingValueChangedWhileDragging = true;
             obj.bindingValueChangedWhileDragging = true;
             obj.bindingValueWhileDragging = (double)e.NewValue;
             obj.bindingValueWhileDragging = (double)e.NewValue;
+            obj.skipSetOpacity = false;
             return;
             return;
         }
         }
         obj.ValueFromSlider = (double)e.NewValue;
         obj.ValueFromSlider = (double)e.NewValue;
+        obj.skipSetOpacity = false;
     }
     }
 
 
     private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
     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)
         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
         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 float Opacity { get; set; }
         
         
         public BlendMode BlendMode { get; set; }
         public BlendMode BlendMode { get; set; }
+        
+        public bool ClipToMemberBelow { get; set; }
 
 
         public bool HasMask => maskBuilder is not null;
         public bool HasMask => maskBuilder is not null;
 
 
@@ -115,6 +117,12 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
             GuidValue = guid;
             GuidValue = guid;
             return this;
             return this;
         }
         }
+
+        public StructureMemberBuilder WithClipToBelow(bool value)
+        {
+            ClipToMemberBelow = value;
+            return this;
+        }
     }
     }
 
 
     public class LayerBuilder : StructureMemberBuilder
     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 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 WithMask(Action<MaskBuilder> mask) => base.WithMask(mask) as LayerBuilder;
         
         
         public new LayerBuilder WithGuid(Guid guid) => base.WithGuid(guid) 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 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)
         public FolderBuilder WithChildren(Action<ChildrenBuilder> children)
         {
         {
             ChildrenBuilder childrenBuilder = new();
             ChildrenBuilder childrenBuilder = new();
@@ -228,7 +240,7 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         {
         {
             if(buffer.IsEmpty) return this;
             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;
             return this;
         }
         }
     }
     }

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

@@ -10,8 +10,7 @@ internal static class PixiParserDocumentEx
     {
     {
         return DocumentViewModel.Build(b =>
         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))
                 .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));
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A));
 
 
@@ -43,6 +42,7 @@ internal static class PixiParserDocumentEx
             .WithOpacity(folder.Opacity)
             .WithOpacity(folder.Opacity)
             .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)folder.BlendMode)
             .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)folder.BlendMode)
             .WithChildren(x => BuildChildren(x, folder.Children))
             .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)));
             .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)
         void BuildLayer(DocumentViewModelBuilder.LayerBuilder builder, ImageLayer layer)
@@ -52,13 +52,16 @@ internal static class PixiParserDocumentEx
                 .WithVisibility(layer.Enabled)
                 .WithVisibility(layer.Enabled)
                 .WithOpacity(layer.Opacity)
                 .WithOpacity(layer.Opacity)
                 .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)layer.BlendMode)
                 .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,
                 .WithMask(layer.Mask,
                     (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
                     (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
                         x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
                         x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
 
 
             if (layer.Width > 0 && layer.Height > 0)
             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
src/PixiEditor/Images/Commands/PixiEditor/Document/Rotate180Deg.png


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


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


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


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


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);
         LoadCommands(serviceProvider, compiledCommandList, commandGroupsData, commands, template);
         LoadTools(serviceProvider, commandGroupsData, commands, template);
         LoadTools(serviceProvider, commandGroupsData, commands, template);
 
 
+        var miscList = new List<Command>();
+
         foreach (var (groupInternalName, storedCommands) in commands)
         foreach (var (groupInternalName, storedCommands) in commands)
         {
         {
             var groupData = commandGroupsData.FirstOrDefault(group => group.internalName == groupInternalName);
             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,
     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) =>
     public override ImageSource CallEvaluate(Command command, object parameter) =>
         base.CallEvaluate(command, parameter ?? command);
         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
             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))
             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 System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
@@ -68,15 +69,31 @@ internal static class ClipboardController
     /// <summary>
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     ///     Pastes image from clipboard into new layer.
     /// </summary>
     /// </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)
         if (images.Count == 0)
             return false;
             return false;
 
 
         if (images.Count == 1)
         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;
             return true;
         }
         }
 
 
@@ -84,12 +101,19 @@ internal static class ClipboardController
         return true;
         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>
     /// <summary>
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
     /// </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();
         List<(string? name, Surface image)> surfaces = new();
 
 
         if (data == null)
         if (data == null)
@@ -121,15 +145,16 @@ internal static class ClipboardController
         return surfaces;
         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;
             return false;
 
 
         try
         try
         {
         {
-            var files = dao.GetFileDropList();
+            var files = dataObject.GetFileDropList();
             if (files != null)
             if (files != null)
             {
             {
                 foreach (var file in files)
                 foreach (var file in files)
@@ -146,8 +171,7 @@ internal static class ClipboardController
             return false;
             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)
     private static BitmapSource FromPNG(DataObject data)
@@ -158,6 +182,8 @@ internal static class ClipboardController
         return decoder.Frames[0];
         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)
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
     {
         try
         try
@@ -168,7 +194,7 @@ internal static class ClipboardController
             {
             {
                 source = FromPNG(data);
                 source = FromPNG(data);
             }
             }
-            else if (data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap))
+            else if (HasData(data, DataFormats.Dib, DataFormats.Bitmap))
             {
             {
                 source = Clipboard.GetImage();
                 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);
         ExportFilePopup popup = new ExportFilePopup(FileWidth, FileHeight);
         popup.ShowDialog();
         popup.ShowDialog();
+
         if (popup.DialogResult == true)
         if (popup.DialogResult == true)
         {
         {
             FileWidth = popup.SaveWidth;
             FileWidth = popup.SaveWidth;
             FileHeight = popup.SaveHeight;
             FileHeight = popup.SaveHeight;
             FilePath = popup.SavePath;
             FilePath = popup.SavePath;
+
             ChosenFormat = popup.SaveFormat;
             ChosenFormat = popup.SaveFormat;
         }
         }
 
 

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

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

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

@@ -89,6 +89,9 @@ internal class ChangeExecutionController
         return true;
         return true;
     }
     }
 
 
+    public void MidChangeUndoInlet() => currentSession?.OnMidChangeUndo();
+    public void MidChangeRedoInlet() => currentSession?.OnMidChangeRedo();
+
     public void ConvertedKeyDownInlet(Key key)
     public void ConvertedKeyDownInlet(Key key)
     {
     {
         currentSession?.OnConvertedKeyDown(key);
         currentSession?.OnConvertedKeyDown(key);
@@ -175,4 +178,9 @@ internal class ChangeExecutionController
     {
     {
         currentSession?.OnLineOverlayMoved(start, end);
         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);
                 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));
         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;
         Internals = internals;
     }
     }
 
 
+    /// <summary>
+    /// Creates a new selection with the size of the document
+    /// </summary>
     public void SelectAll()
     public void SelectAll()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -33,6 +36,9 @@ internal class DocumentOperationsModule
             new EndSelectRectangle_Action());
             new EndSelectRectangle_Action());
     }
     }
 
 
+    /// <summary>
+    /// Clears the current selection
+    /// </summary>
     public void ClearSelection()
     public void ClearSelection()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -40,6 +46,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClearSelection_Action());
         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)
     public void DeleteSelectedPixels(bool clearSelection = false)
     {
     {
         var member = Document.SelectedStructureMember;
         var member = Document.SelectedStructureMember;
@@ -54,6 +64,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions();
         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)
     public void SetMemberOpacity(Guid memberGuid, float value)
     {
     {
         if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
         if (Internals.ChangeController.IsChangeActive || value is > 1 or < 0)
@@ -63,10 +78,20 @@ internal class DocumentOperationsModule
             new EndStructureMemberOpacity_Action());
             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));
     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));
     public void RemoveViewport(Guid viewportGuid) => Internals.ActionAccumulator.AddActions(new RemoveViewport_PassthroughAction(viewportGuid));
 
 
+    /// <summary>
+    /// Delete the whole undo stack
+    /// </summary>
     public void ClearUndo()
     public void ClearUndo()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -74,6 +99,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new DeleteRecordedChanges_Action());
         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)
     public void PasteImagesAsLayers(List<(string? name, Surface image)> images)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -90,19 +119,29 @@ internal class DocumentOperationsModule
 
 
         foreach (var imageWithName in images)
         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);
             DrawImage(imageWithName.image, new ShapeCorners(new RectD(VecD.Zero, imageWithName.image.Size)), layerGuid, true, false, false);
         }
         }
         Internals.ActionAccumulator.AddFinishedActions();
         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)
         if (Internals.ChangeController.IsChangeActive)
             return null;
             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)
     public void DuplicateLayer(Guid guidValue)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -110,6 +149,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
         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)
     public void DeleteStructureMember(Guid guidValue)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -117,6 +160,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMember_Action(guidValue));
         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)
     public void DeleteStructureMembers(IReadOnlyList<Guid> guids)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -124,6 +171,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(guids.Select(static guid => new DeleteStructureMember_Action(guid)).ToArray());
         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)
     public void ResizeCanvas(VecI newSize, ResizeAnchor anchor)
     {
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
         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));
         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)
     public void ResizeImage(VecI newSize, ResamplingMethod resampling)
     {
     {
         if (Internals.ChangeController.IsChangeActive || newSize.X > 9999 || newSize.Y > 9999 || newSize.X < 1 || newSize.Y < 1)
         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));
         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)
     public void ReplaceColor(Color oldColor, Color newColor)
     {
     {
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
         if (Internals.ChangeController.IsChangeActive || oldColor == newColor)
@@ -175,6 +237,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
         Internals.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
     }
     }
 
 
+    /// <summary>
+    /// Creates a new mask on the <paramref name="member"/>
+    /// </summary>
     public void CreateMask(StructureMemberViewModel member)
     public void CreateMask(StructureMemberViewModel member)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -184,6 +249,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CreateStructureMemberMask_Action(member.GuidValue));
         Internals.ActionAccumulator.AddFinishedActions(new CreateStructureMemberMask_Action(member.GuidValue));
     }
     }
 
 
+    /// <summary>
+    /// Deletes the mask of the <paramref name="member"/>
+    /// </summary>
     public void DeleteMask(StructureMemberViewModel member)
     public void DeleteMask(StructureMemberViewModel member)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -191,6 +259,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
         Internals.ActionAccumulator.AddFinishedActions(new DeleteStructureMemberMask_Action(member.GuidValue));
     }
     }
     
     
+    /// <summary>
+    /// Applies the mask to the image
+    /// </summary>
     public void ApplyMask(StructureMemberViewModel member)
     public void ApplyMask(StructureMemberViewModel member)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -199,28 +270,69 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ApplyMask_Action(member.GuidValue), new DeleteStructureMemberMask_Action(member.GuidValue));
         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));
     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));
     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));
     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());
     public void ClearSoftSelectedMembers() => Internals.ActionAccumulator.AddActions(new ClearSoftSelectedMembers_PassthroughAction());
 
 
+    /// <summary>
+    /// Undo last change
+    /// </summary>
     public void Undo()
     public void Undo()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeUndoInlet();
             return;
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Undo_Action());
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
     }
 
 
+    /// <summary>
+    /// Redo previously undone change
+    /// </summary>
     public void Redo()
     public void Redo()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
+        {
+            Internals.ChangeController.MidChangeRedoInlet();
             return;
             return;
+        }
         Internals.ActionAccumulator.AddActions(new Redo_Action());
         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)
     public void MoveStructureMember(Guid memberToMove, Guid memberToMoveIntoOrNextTo, StructureMemberPlacement placement)
     {
     {
         if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
         if (Internals.ChangeController.IsChangeActive || memberToMove == memberToMoveIntoOrNextTo)
@@ -228,6 +340,9 @@ internal class DocumentOperationsModule
         Internals.StructureHelper.TryMoveStructureMember(memberToMove, memberToMoveIntoOrNextTo, placement);
         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)
     public void MergeStructureMembers(IReadOnlyList<Guid> members)
     {
     {
         if (Internals.ChangeController.IsChangeActive || members.Count < 2)
         if (Internals.ChangeController.IsChangeActive || members.Count < 2)
@@ -248,6 +363,11 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddActions(new ChangeBoundary_Action());
         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)
     public void PasteImageWithTransform(Surface image, VecI startPos)
     {
     {
         if (Document.SelectedStructureMember is null)
         if (Document.SelectedStructureMember is null)
@@ -255,6 +375,20 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos));
         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)
     public void TransformSelectedArea(bool toolLinked)
     {
     {
         if (Document.SelectedStructureMember is null ||
         if (Document.SelectedStructureMember is null ||
@@ -264,6 +398,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
         Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
     }
     }
 
 
+    /// <summary>
+    /// Ties stopping the currently executing tool linked executor
+    /// </summary>
     public void TryStopToolLinkedExecutor()
     public void TryStopToolLinkedExecutor()
     {
     {
         if (Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked)
         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) =>
     public void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask) =>
         DrawImage(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, true);
         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)
     private void DrawImage(Surface image, ShapeCorners corners, Guid memberGuid, bool ignoreClipSymmetriesEtc, bool drawOnMask, bool finish)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -284,6 +430,9 @@ internal class DocumentOperationsModule
             Internals.ActionAccumulator.AddFinishedActions();
             Internals.ActionAccumulator.AddFinishedActions();
     }
     }
 
 
+    /// <summary>
+    /// Resizes the canvas to fit the content
+    /// </summary>
     public void ClipCanvas()
     public void ClipCanvas()
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -291,8 +440,14 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
         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);
     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)
     public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -301,8 +456,16 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
         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);
     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)
     public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -311,6 +474,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
         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)
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -319,6 +485,10 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList()));
         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)
     public void ImportReferenceLayer(ImmutableArray<byte> imagePbgra32Bytes, VecI imageSize)
     {
     {
         if (Internals.ChangeController.IsChangeActive)
         if (Internals.ChangeController.IsChangeActive)
@@ -329,6 +499,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imagePbgra32Bytes, imageSize));
         Internals.ActionAccumulator.AddFinishedActions(new SetReferenceLayer_Action(corners, imagePbgra32Bytes, imageSize));
     }
     }
 
 
+    /// <summary>
+    /// Deletes the reference layer
+    /// </summary>
     public void DeleteReferenceLayer()
     public void DeleteReferenceLayer()
     {
     {
         if (Internals.ChangeController.IsChangeActive || Document.ReferenceLayerViewModel.ReferenceBitmap is null)
         if (Internals.ChangeController.IsChangeActive || Document.ReferenceLayerViewModel.ReferenceBitmap is null)
@@ -337,6 +510,9 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new DeleteReferenceLayer_Action());
         Internals.ActionAccumulator.AddFinishedActions(new DeleteReferenceLayer_Action());
     }
     }
 
 
+    /// <summary>
+    /// Starts a transform on the reference layer
+    /// </summary>
     public void TransformReferenceLayer()
     public void TransformReferenceLayer()
     {
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
@@ -344,6 +520,9 @@ internal class DocumentOperationsModule
         Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
         Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
     }
     }
 
 
+    /// <summary>
+    /// Resets the reference layer transform
+    /// </summary>
     public void ResetReferenceLayerPosition()
     public void ResetReferenceLayerPosition()
     {
     {
         if (Document.ReferenceLayerViewModel.ReferenceBitmap is null || Internals.ChangeController.IsChangeActive)
         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()
     public override ExecutionState Start()
     {
     {
-        mode = ViewModelMain.Current?.ToolsSubViewModel.GetTool<LassoToolViewModel>()?.SelectMode;
+        mode = ViewModelMain.Current?.ToolsSubViewModel.GetTool<LassoToolViewModel>()?.ResultingSelectionMode;
 
 
         if (mode is null)
         if (mode is null)
             return ExecutionState.Error;
             return ExecutionState.Error;

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

@@ -55,11 +55,11 @@ internal class LineToolExecutor : UpdateableChangeExecutor
         if (transforming)
         if (transforming)
             return;
             return;
         started = true;
         started = true;
-        curPos = pos;
-        VecI targetPos = pos;
+
         if (toolViewModel!.Snap)
         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()
     public override void OnLeftMouseButtonUp()
@@ -69,10 +69,8 @@ internal class LineToolExecutor : UpdateableChangeExecutor
             onEnded!(this);
             onEnded!(this);
             return;
             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;
         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));
         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()
     public override void OnTransformApplied()
     {
     {
         if (!transforming)
         if (!transforming)
             return;
             return;
 
 
-        document!.LineToolOverlayViewModel.IsEnabled = false;
+        document!.LineToolOverlayViewModel.Hide();
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         onEnded!(this);
         onEnded!(this);
     }
     }
@@ -96,7 +115,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
     public override void ForceStop()
     public override void ForceStop()
     {
     {
         if (transforming)
         if (transforming)
-            document!.LineToolOverlayViewModel.IsEnabled = false;
+            document!.LineToolOverlayViewModel.Hide();
 
 
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
         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 Surface image;
     private readonly VecI pos;
     private readonly VecI pos;
     private bool drawOnMask;
     private bool drawOnMask;
-    private Guid memberGuid;
+    private Guid? memberGuid;
 
 
     public PasteImageExecutor(Surface image, VecI pos)
     public PasteImageExecutor(Surface image, VecI pos)
     {
     {
@@ -19,32 +19,52 @@ internal class PasteImageExecutor : UpdateableChangeExecutor
         this.pos = pos;
         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()
     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));
         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;
         return ExecutionState.Success;
     }
     }
 
 
     public override void OnTransformMoved(ShapeCorners corners)
     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()
     public override void OnTransformApplied()
     {
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndPasteImage_Action());
         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;
         startPos = controller!.LastPixelPosition;
         selectShape = toolViewModel.SelectShape;
         selectShape = toolViewModel.SelectShape;
-        selectMode = toolViewModel.SelectMode;
+        selectMode = toolViewModel.ResultingSelectionMode;
 
 
         IAction action = CreateUpdateAction(selectShape, new RectI(startPos, new(0)), selectMode);
         IAction action = CreateUpdateAction(selectShape, new RectI(startPos, new(0)), selectMode);
         internals!.ActionAccumulator.AddActions(action);
         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);
         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)
     public override void OnPixelPositionChange(VecI pos)
     {
     {
         if (transforming)
         if (transforming)
@@ -133,7 +154,7 @@ internal abstract class ShapeToolExecutor<T> : UpdateableChangeExecutor where T
             return;
             return;
         }
         }
         transforming = true;
         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()
     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.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -15,7 +16,7 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
         ShapeCorners corners = document.ReferenceLayerViewModel.ReferenceShapeBindable;
         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));
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
@@ -25,6 +26,12 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
         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()
     public override void OnTransformApplied()
     {
     {
         internals!.ActionAccumulator.AddFinishedActions(new EndTransformReferenceLayer_Action());
         internals!.ActionAccumulator.AddFinishedActions(new EndTransformReferenceLayer_Action());

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

@@ -1,4 +1,5 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
@@ -32,7 +33,7 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
         ShapeCorners corners = new(document.SelectionPathBindable.TightBounds);
         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();
         membersToTransform = members.Select(static a => a.GuidValue).ToArray();
         internals!.ActionAccumulator.AddActions(
         internals!.ActionAccumulator.AddActions(
             new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false));
             new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false));
@@ -45,8 +46,17 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false));
             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()
     public override void OnTransformApplied()
     {
     {
+        if (Type == ExecutorType.ToolLinked)
+            return;
+
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformViewModel.HideTransform();
         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 OnTransformMoved(ShapeCorners corners) { }
     public virtual void OnTransformApplied() { }
     public virtual void OnTransformApplied() { }
     public virtual void OnLineOverlayMoved(VecD start, VecD end) { }
     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 ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -11,15 +12,15 @@ using PixiEditor.DrawingApi.Core.Numerics;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
 #nullable enable
 #nullable enable
-internal class AffectedChunkGatherer
+internal class AffectedAreasGatherer
 {
 {
     private readonly DocumentChangeTracker tracker;
     private readonly DocumentChangeTracker tracker;
 
 
-    public HashSet<VecI> MainImageChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> ImagePreviewChunks { get; private set; } = new();
-    public Dictionary<Guid, HashSet<VecI>> MaskPreviewChunks { get; private set; } = new();
+    public AffectedArea MainImageArea { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> ImagePreviewAreas { get; private set; } = new();
+    public Dictionary<Guid, AffectedArea> MaskPreviewAreas { get; private set; } = new();
 
 
-    public AffectedChunkGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
+    public AffectedAreasGatherer(DocumentChangeTracker tracker, IReadOnlyList<IChangeInfo> changes)
     {
     {
         this.tracker = tracker;
         this.tracker = tracker;
         ProcessChanges(changes);
         ProcessChanges(changes);
@@ -31,18 +32,18 @@ internal class AffectedChunkGatherer
         {
         {
             switch (change)
             switch (change)
             {
             {
-                case MaskChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case MaskArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks, true);
-                    AddToMaskPreview(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area, true);
+                    AddToMaskPreview(info.GuidValue, info.Area);
                     break;
                     break;
-                case LayerImageChunks_ChangeInfo info:
-                    if (info.Chunks is null)
+                case LayerImageArea_ChangeInfo info:
+                    if (info.Area.Chunks is null)
                         throw new InvalidOperationException("Chunks must not be null");
                         throw new InvalidOperationException("Chunks must not be null");
-                    AddToMainImage(info.Chunks);
-                    AddToImagePreviews(info.GuidValue, info.Chunks);
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.GuidValue, info.Area);
                     break;
                     break;
                 case CreateStructureMember_ChangeInfo info:
                 case CreateStructureMember_ChangeInfo info:
                     AddAllToMainImage(info.GuidValue);
                     AddAllToMainImage(info.GuidValue);
@@ -99,7 +100,7 @@ internal class AffectedChunkGatherer
         if (member is IReadOnlyLayer layer)
         if (member is IReadOnlyLayer layer)
         {
         {
             var chunks = layer.LayerImage.FindAllChunks();
             var chunks = layer.LayerImage.FindAllChunks();
-            AddToImagePreviews(memberGuid, chunks, ignoreSelf);
+            AddToImagePreviews(memberGuid, new AffectedArea(chunks), ignoreSelf);
         }
         }
         else if (member is IReadOnlyFolder folder)
         else if (member is IReadOnlyFolder folder)
         {
         {
@@ -117,7 +118,7 @@ internal class AffectedChunkGatherer
             var chunks = layer.LayerImage.FindAllChunks();
             var chunks = layer.LayerImage.FindAllChunks();
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
             if (layer.Mask is not null && layer.MaskIsVisible && useMask)
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
                 chunks.IntersectWith(layer.Mask.FindAllChunks());
-            AddToMainImage(chunks);
+            AddToMainImage(new AffectedArea(chunks));
         }
         }
         else
         else
         {
         {
@@ -132,7 +133,7 @@ internal class AffectedChunkGatherer
         if (member.Mask is not null)
         if (member.Mask is not null)
         {
         {
             var chunks = member.Mask.FindAllChunks();
             var chunks = member.Mask.FindAllChunks();
-            AddToMaskPreview(memberGuid, chunks);
+            AddToMaskPreview(memberGuid, new AffectedArea(chunks));
         }
         }
         if (member is IReadOnlyFolder folder)
         if (member is IReadOnlyFolder folder)
         {
         {
@@ -142,12 +143,14 @@ internal class AffectedChunkGatherer
     }
     }
 
 
 
 
-    private void AddToMainImage(HashSet<VecI> chunks)
+    private void AddToMainImage(AffectedArea area)
     {
     {
-        MainImageChunks.UnionWith(chunks);
+        var temp = MainImageArea;
+        temp.UnionWith(area);
+        MainImageArea = temp;
     }
     }
 
 
-    private void AddToImagePreviews(Guid memberGuid, HashSet<VecI> chunks, bool ignoreSelf = false)
+    private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     {
     {
         var path = tracker.Document.FindMemberPath(memberGuid);
         var path = tracker.Document.FindMemberPath(memberGuid);
         if (path.Count < 2)
         if (path.Count < 2)
@@ -155,25 +158,37 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
         {
             var member = path[i];
             var member = path[i];
-            if (!ImagePreviewChunks.ContainsKey(member.GuidValue))
-                ImagePreviewChunks[member.GuidValue] = new HashSet<VecI>(chunks);
+            if (!ImagePreviewAreas.ContainsKey(member.GuidValue))
+            {
+                ImagePreviewAreas[member.GuidValue] = new AffectedArea(area);
+            }
             else
             else
-                ImagePreviewChunks[member.GuidValue].UnionWith(chunks);
+            {
+                var temp = ImagePreviewAreas[member.GuidValue];
+                temp.UnionWith(area);
+                ImagePreviewAreas[member.GuidValue] = temp;
+            }
         }
         }
     }
     }
 
 
-    private void AddToMaskPreview(Guid memberGuid, HashSet<VecI> chunks)
+    private void AddToMaskPreview(Guid memberGuid, AffectedArea area)
     {
     {
-        if (!MaskPreviewChunks.ContainsKey(memberGuid))
-            MaskPreviewChunks[memberGuid] = new HashSet<VecI>(chunks);
+        if (!MaskPreviewAreas.ContainsKey(memberGuid))
+        {
+            MaskPreviewAreas[memberGuid] = new AffectedArea(area);
+        }
         else
         else
-            MaskPreviewChunks[memberGuid].UnionWith(chunks);
+        {
+            var temp = MaskPreviewAreas[memberGuid];
+            temp.UnionWith(area);
+            MaskPreviewAreas[memberGuid] = temp;
+        }
     }
     }
 
 
 
 
     private void AddWholeCanvasToMainImage()
     private void AddWholeCanvasToMainImage()
     {
     {
-        AddAllChunks(MainImageChunks);
+        MainImageArea = AddWholeArea(MainImageArea);
     }
     }
 
 
     private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
     private void AddWholeCanvasToImagePreviews(Guid memberGuid, bool ignoreSelf = false)
@@ -185,17 +200,17 @@ internal class AffectedChunkGatherer
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         for (int i = ignoreSelf ? 1 : 0; i < path.Count - 1; i++)
         {
         {
             var member = path[i];
             var member = path[i];
-            if (!ImagePreviewChunks.ContainsKey(member.GuidValue))
-                ImagePreviewChunks[member.GuidValue] = new HashSet<VecI>();
-            AddAllChunks(ImagePreviewChunks[member.GuidValue]);
+            if (!ImagePreviewAreas.ContainsKey(member.GuidValue))
+                ImagePreviewAreas[member.GuidValue] = new AffectedArea();
+            ImagePreviewAreas[member.GuidValue] = AddWholeArea(ImagePreviewAreas[member.GuidValue]);
         }
         }
     }
     }
 
 
     private void AddWholeCanvasToMaskPreview(Guid memberGuid)
     private void AddWholeCanvasToMaskPreview(Guid memberGuid)
     {
     {
-        if (!MaskPreviewChunks.ContainsKey(memberGuid))
-            MaskPreviewChunks[memberGuid] = new HashSet<VecI>();
-        AddAllChunks(MaskPreviewChunks[memberGuid]);
+        if (!MaskPreviewAreas.ContainsKey(memberGuid))
+            MaskPreviewAreas[memberGuid] = new AffectedArea();
+        MaskPreviewAreas[memberGuid] = AddWholeArea(MaskPreviewAreas[memberGuid]);
     }
     }
 
 
 
 
@@ -209,7 +224,7 @@ internal class AffectedChunkGatherer
         tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
         tracker.Document.ForEveryReadonlyMember((member) => AddWholeCanvasToMaskPreview(member.GuidValue));
     }
     }
 
 
-    private void AddAllChunks(HashSet<VecI> chunks)
+    private AffectedArea AddWholeArea(AffectedArea area)
     {
     {
         VecI size = new(
         VecI size = new(
             (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
             (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
@@ -218,8 +233,10 @@ internal class AffectedChunkGatherer
         {
         {
             for (int j = 0; j < size.Y; j++)
             for (int j = 0; j < size.Y; j++)
             {
             {
-                chunks.Add(new(i, j));
+                area.Chunks.Add(new(i, j));
             }
             }
         }
         }
+        area.GlobalArea = new RectI(VecI.Zero, tracker.Document.Size);
+        return area;
     }
     }
 }
 }

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

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

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

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

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

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

+ 13 - 1
src/PixiEditor/PixiEditor.csproj

@@ -218,7 +218,7 @@
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
 		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
 		<PackageReference Include="OneOf" Version="3.0.223" />
 		<PackageReference Include="OneOf" Version="3.0.223" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
 		<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="PixiEditor.Parser.Skia" Version="3.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
 		<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
 		<PackageReference Include="WpfAnimatedGif" Version="2.0.2" />
@@ -356,6 +356,18 @@
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleMask.png" />
 		<Resource Include="Images\Commands\PixiEditor\Layer\ToggleMask.png" />
 		<None Remove="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
 		<None Remove="Images\Commands\PixiEditor\Layer\ToggleVisible.png" />
 		<Resource Include="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>
 	<ItemGroup>
 	<ItemGroup>
 		<None Include="..\LICENSE">
 		<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
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [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;
     public bool DocumentNotNull() => ActiveDocument != null;
 
 
     [Command.Basic("PixiEditor.Document.ClipCanvas", "Clip Canvas", "Clip Canvas", CanExecute = "PixiEditor.HasDocument")]
     [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)
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
             return;
         
         
-        ActiveDocument?.Operations.FlipImage(FlipType.Vertical, ActiveDocument.GetSelectedMembers());
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers());
     }
     }
     
     
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
     [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)]
         "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
         "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
         "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", 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
         "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
         "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();
         var root = new Folder();
         
         
-        IReadOnlyDocument doc = Internals.Tracker.Document;
+        var doc = Internals.Tracker.Document;
 
 
         AddMembers(doc.StructureRoot.Children, doc, root);
         AddMembers(doc.StructureRoot.Children, doc, root);
 
 
@@ -31,7 +31,7 @@ internal partial class DocumentViewModel
         {
         {
             Width = Width, Height = Height,
             Width = Width, Height = Height,
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
             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;
         return document;
@@ -64,6 +64,7 @@ internal partial class DocumentViewModel
             BlendMode = (BlendMode)(int)folder.BlendMode,
             BlendMode = (BlendMode)(int)folder.BlendMode,
             Enabled = folder.IsVisible,
             Enabled = folder.IsVisible,
             Opacity = folder.Opacity,
             Opacity = folder.Opacity,
+            ClipToMemberBelow = folder.ClipToMemberBelow,
             Mask = GetMask(folder.Mask, folder.MaskIsVisible)
             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,
             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,
             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;
         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;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document.TransformOverlays;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
@@ -177,6 +178,10 @@ internal partial class DocumentViewModel : NotifyableObject
         ReferenceLayerViewModel = new(this, Internals);
         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)
     public static DocumentViewModel Build(Action<DocumentViewModelBuilder> builder)
     {
     {
         var builderInstance = new DocumentViewModelBuilder();
         var builderInstance = new DocumentViewModelBuilder();
@@ -215,6 +220,10 @@ internal partial class DocumentViewModel : NotifyableObject
 
 
             if (!member.IsVisible)
             if (!member.IsVisible)
                 acc.AddActions(new StructureMemberIsVisible_Action(member.IsVisible, member.GuidValue));
                 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)
             if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             {
             {
@@ -278,6 +287,10 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
         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()
     public OneOf<Error, Surface> MaybeRenderWholeImage()
     {
     {
         try
         try
@@ -356,6 +369,11 @@ internal partial class DocumentViewModel : NotifyableObject
         return (output, bounds);
         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)
     public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas)
     {
     {
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
@@ -401,7 +419,7 @@ internal partial class DocumentViewModel : NotifyableObject
             if (scope == DocumentScope.AllLayers)
             if (scope == DocumentScope.AllLayers)
             {
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot)
+                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot, new RectI(pos, VecI.One))
                     .Match<Color>(
                     .Match<Color>(
                         (Chunk chunk) =>
                         (Chunk chunk) =>
                         {
                         {
@@ -485,6 +503,9 @@ internal partial class DocumentViewModel : NotifyableObject
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
     #endregion
 
 
+    /// <summary>
+    /// Returns a list of all selected members (Hard and Soft selected)
+    /// </summary>
     public List<Guid> GetSelectedMembers()
     public List<Guid> GetSelectedMembers()
     {
     {
         List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
         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);
-    }
-}

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