Browse Source

Refactoring, ChunkyImage operation, Apply mask change

Equbuxu 3 years ago
parent
commit
227388b208
41 changed files with 406 additions and 144 deletions
  1. 49 9
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 1
      src/ChunkyImageLib/CommittedChunkStorage.cs
  3. 6 0
      src/ChunkyImageLib/DataHolders/VecD.cs
  4. 12 2
      src/ChunkyImageLib/DataHolders/VecI.cs
  5. 106 0
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  6. 2 0
      src/ChunkyImageLib/Operations/OperationHelper.cs
  7. 2 1
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  8. 9 8
      src/ChunkyImageLibTest/ChunkyImageTests.cs
  9. 3 8
      src/PixiEditor.ChangeableDocument/Changes/Change.cs
  10. 87 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  11. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs
  12. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  13. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  15. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkStorage.cs
  16. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  17. 3 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  18. 12 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs
  19. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs
  20. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs
  21. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerLockTransparency_Change.cs
  22. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberBlendMode_Change.cs
  23. 2 9
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberClipToMemberBelow_Change.cs
  24. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs
  25. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs
  26. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs
  27. 2 11
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  28. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs
  29. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisState_Change.cs
  30. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  31. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs
  32. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  33. 2 5
      src/PixiEditor.ChangeableDocument/Changes/UpdateableChange.cs
  34. 40 24
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  35. 1 0
      src/PixiEditor.ChangeableDocument/GlobalUsings.cs
  36. 6 10
      src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs
  37. 3 3
      src/PixiEditor.Zoombox/Zoombox.xaml.cs
  38. 2 2
      src/PixiEditorPrototype/Models/Rendering/AffectedChunkGatherer.cs
  39. 9 0
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  40. 3 0
      src/PixiEditorPrototype/Views/MainWindow.xaml
  41. 2 0
      src/README.md

+ 49 - 9
src/ChunkyImageLib/ChunkyImage.cs

@@ -49,7 +49,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     private object lockObject = new();
     private object lockObject = new();
     private int commitCounter = 0;
     private int commitCounter = 0;
 
 
-    public static int ChunkSize => ChunkPool.FullChunkSize;
+    public const int FullChunkSize = ChunkPool.FullChunkSize;
     private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
     private static SKPaint ClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstIn };
     private static SKPaint InverseClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstOut };
     private static SKPaint InverseClippingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.DstOut };
     private static SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
     private static SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
@@ -101,6 +101,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ChunkyImage output = new(LatestSize);
             ChunkyImage output = new(LatestSize);
             var chunks = FindCommittedChunks();
             var chunks = FindCommittedChunks();
             foreach (var chunk in chunks)
             foreach (var chunk in chunks)
@@ -108,7 +109,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 var image = GetCommittedChunk(chunk, ChunkResolution.Full);
                 var image = GetCommittedChunk(chunk, ChunkResolution.Full);
                 if (image is null)
                 if (image is null)
                     continue;
                     continue;
-                output.EnqueueDrawImage(chunk * ChunkSize, image.Surface);
+                output.EnqueueDrawImage(chunk * FullChunkSize, image.Surface);
             }
             }
             output.CommitChanges();
             output.CommitChanges();
             return output;
             return output;
@@ -122,6 +123,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             var latestChunk = GetLatestChunk(chunkPos, resolution);
             var latestChunk = GetLatestChunk(chunkPos, resolution);
             var committedChunk = GetCommittedChunk(chunkPos, resolution);
             var committedChunk = GetCommittedChunk(chunkPos, resolution);
 
 
@@ -159,6 +161,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
             if (MaybeGetLatestChunk(chunkPos, ChunkResolution.Full) is not null ||
                 MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
                 MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null)
                 return true;
                 return true;
@@ -175,6 +178,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             var chunk = GetCommittedChunk(chunkPos, resolution);
             var chunk = GetCommittedChunk(chunkPos, resolution);
             if (chunk is null)
             if (chunk is null)
                 return false;
                 return false;
@@ -187,6 +191,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
             return MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is not null;
         }
         }
     }
     }
@@ -229,6 +234,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             if (queuedOperations.Count > 0)
             if (queuedOperations.Count > 0)
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
             activeClips.Add(clippingMask);
             activeClips.Add(clippingMask);
@@ -242,6 +248,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             if (queuedOperations.Count > 0)
             if (queuedOperations.Count > 0)
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
             blendMode = mode;
             blendMode = mode;
@@ -252,6 +259,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             if (queuedOperations.Count > 0)
             if (queuedOperations.Count > 0)
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
             horizontalSymmetryAxis = position;
             horizontalSymmetryAxis = position;
@@ -262,6 +270,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             if (queuedOperations.Count > 0)
             if (queuedOperations.Count > 0)
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
                 throw new InvalidOperationException("This function can only be executed when there are no queued operations");
             verticalSymmetryAxis = position;
             verticalSymmetryAxis = position;
@@ -272,6 +281,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             lockTransparency = true;
             lockTransparency = true;
         }
         }
     }
     }
@@ -280,6 +290,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             RectangleOperation operation = new(rect);
             RectangleOperation operation = new(rect);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
@@ -295,6 +306,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ImageOperation operation = new(corners, image, copyImage);
             ImageOperation operation = new(corners, image, copyImage);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
@@ -310,15 +322,27 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ImageOperation operation = new(pos, image, copyImage);
             ImageOperation operation = new(pos, image, copyImage);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
     }
     }
 
 
+    public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            ChunkyImageOperation operation = new(image, pos, flipHor, flipVer);
+            EnqueueOperation(operation);
+        }
+    }
+
     public void EnqueueClearRegion(VecI pos, VecI size)
     public void EnqueueClearRegion(VecI pos, VecI size)
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ClearRegionOperation operation = new(pos, size);
             ClearRegionOperation operation = new(pos, size);
             EnqueueOperation(operation);
             EnqueueOperation(operation);
         }
         }
@@ -328,6 +352,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ClearOperation operation = new();
             ClearOperation operation = new();
             EnqueueOperation(operation, FindAllChunks());
             EnqueueOperation(operation, FindAllChunks());
         }
         }
@@ -337,6 +362,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             ResizeOperation operation = new(newSize);
             ResizeOperation operation = new(newSize);
             LatestSize = newSize;
             LatestSize = newSize;
             EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
             EnqueueOperation(operation, FindAllChunksOutsideBounds(newSize));
@@ -374,9 +400,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             //clear queued operations
             //clear queued operations
             foreach (var operation in queuedOperations)
             foreach (var operation in queuedOperations)
-                operation.Item1.Dispose();
+                operation.operation.Dispose();
             queuedOperations.Clear();
             queuedOperations.Clear();
 
 
             //clear additional state
             //clear additional state
@@ -407,6 +434,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             var affectedChunks = FindAffectedChunks();
             var affectedChunks = FindAffectedChunks();
             foreach (var chunk in affectedChunks)
             foreach (var chunk in affectedChunks)
             {
             {
@@ -543,6 +571,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            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 (_, opChunks) in queuedOperations)
             {
             {
@@ -556,6 +585,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
             return committedChunks[ChunkResolution.Full].Select(chunk => chunk.Key).ToHashSet();
         }
         }
     }
     }
@@ -567,6 +597,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
             var chunks = new HashSet<VecI>();
             var chunks = new HashSet<VecI>();
             foreach (var (_, opChunks) in queuedOperations)
             foreach (var (_, opChunks) in queuedOperations)
             {
             {
@@ -642,7 +673,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         {
         {
             if (mask.CommittedChunkExists(chunkPos))
             if (mask.CommittedChunkExists(chunkPos))
             {
             {
-                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.SkiaSurface, new(0, 0), ClippingPaint);
+                mask.DrawCommittedChunkOn(chunkPos, resolution, intersection.Surface.SkiaSurface, VecI.Zero, ClippingPaint);
             }
             }
             else
             else
             {
             {
@@ -686,14 +717,14 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             var clip = combinedClips.AsT2;
             var clip = combinedClips.AsT2;
 
 
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
-            targetChunk.DrawOnSurface(tempChunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint);
+            targetChunk.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ReplacingPaint);
 
 
             chunkOperation.DrawOnChunk(tempChunk, chunkPos);
             chunkOperation.DrawOnChunk(tempChunk, chunkPos);
 
 
-            clip.DrawOnSurface(tempChunk.Surface.SkiaSurface, new(0, 0), ClippingPaint);
-            clip.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), InverseClippingPaint);
+            clip.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ClippingPaint);
+            clip.DrawOnSurface(targetChunk.Surface.SkiaSurface, VecI.Zero, InverseClippingPaint);
 
 
-            tempChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, new(0, 0), AddingPaint);
+            tempChunk.DrawOnSurface(targetChunk.Surface.SkiaSurface, VecI.Zero, AddingPaint);
             return false;
             return false;
         }
         }
 
 
@@ -712,6 +743,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     {
     {
         lock (lockObject)
         lock (lockObject)
         {
         {
+            ThrowIfDisposed();
+            if (queuedOperations.Count > 0)
+                throw new InvalidOperationException("This function can only be used when there are no queued operations");
             FindAndDeleteEmptyCommittedChunks();
             FindAndDeleteEmptyCommittedChunks();
             return committedChunks[ChunkResolution.Full].Count == 0;
             return committedChunks[ChunkResolution.Full].Count == 0;
         }
         }
@@ -726,7 +760,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
 
     private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
     private static bool IsOutsideBounds(VecI chunkPos, VecI imageSize)
     {
     {
-        return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * ChunkSize >= imageSize.X || chunkPos.Y * ChunkSize >= imageSize.Y;
+        return chunkPos.X < 0 || chunkPos.Y < 0 || chunkPos.X * FullChunkSize >= imageSize.X || chunkPos.Y * FullChunkSize >= imageSize.Y;
     }
     }
 
 
     private void FindAndDeleteEmptyCommittedChunks()
     private void FindAndDeleteEmptyCommittedChunks()
@@ -836,6 +870,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         return newLatestChunk;
         return newLatestChunk;
     }
     }
 
 
+    private void ThrowIfDisposed()
+    {
+        if (disposed)
+            throw new ObjectDisposedException(nameof(ChunkyImage));
+    }
+
     public void Dispose()
     public void Dispose()
     {
     {
         lock (lockObject)
         lock (lockObject)

+ 1 - 1
src/ChunkyImageLib/CommittedChunkStorage.cs

@@ -14,7 +14,7 @@ public class CommittedChunkStorage : IDisposable
         foreach (var chunkPos in committedChunksToSave)
         foreach (var chunkPos in committedChunksToSave)
         {
         {
             Chunk copy = Chunk.Create();
             Chunk copy = Chunk.Create();
-            if (!image.DrawCommittedChunkOn(chunkPos, ChunkResolution.Full, copy.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+            if (!image.DrawCommittedChunkOn(chunkPos, ChunkResolution.Full, copy.Surface.SkiaSurface, VecI.Zero, ReplacingPaint))
             {
             {
                 copy.Dispose();
                 copy.Dispose();
                 savedChunks.Add((chunkPos, null));
                 savedChunks.Add((chunkPos, null));

+ 6 - 0
src/ChunkyImageLib/DataHolders/VecD.cs

@@ -14,6 +14,8 @@ public struct VecD
     public double LongestAxis => (Math.Abs(X) < Math.Abs(Y)) ? Y : X;
     public double LongestAxis => (Math.Abs(X) < Math.Abs(Y)) ? Y : X;
     public double ShortestAxis => (Math.Abs(X) < Math.Abs(Y)) ? X : Y;
     public double ShortestAxis => (Math.Abs(X) < Math.Abs(Y)) ? X : Y;
 
 
+    public static VecD Zero { get; } = new(0, 0);
+
     public VecD(double x, double y)
     public VecD(double x, double y)
     {
     {
         X = x;
         X = x;
@@ -163,6 +165,10 @@ public struct VecD
     {
     {
         return new VecD(a.X / b, a.Y / b);
         return new VecD(a.X / b, a.Y / b);
     }
     }
+    public static VecD operator %(VecD a, double b)
+    {
+        return new(a.X % b, a.Y % b);
+    }
     public static bool operator ==(VecD a, VecD b)
     public static bool operator ==(VecD a, VecD b)
     {
     {
         return a.X == b.X && a.Y == b.Y;
         return a.X == b.X && a.Y == b.Y;

+ 12 - 2
src/ChunkyImageLib/DataHolders/VecI.cs

@@ -13,6 +13,8 @@ public struct VecI
     public int LongestAxis => (Math.Abs(X) < Math.Abs(Y)) ? Y : X;
     public int LongestAxis => (Math.Abs(X) < Math.Abs(Y)) ? Y : X;
     public int ShortestAxis => (Math.Abs(X) < Math.Abs(Y)) ? X : Y;
     public int ShortestAxis => (Math.Abs(X) < Math.Abs(Y)) ? X : Y;
 
 
+    public static VecI Zero { get; } = new(0, 0);
+
     public VecI(int x, int y)
     public VecI(int x, int y)
     {
     {
         X = x;
         X = x;
@@ -28,14 +30,14 @@ public struct VecI
         return new VecI(X * other.X, Y * other.Y);
         return new VecI(X * other.X, Y * other.Y);
     }
     }
     /// <summary>
     /// <summary>
-    /// Reflects the vector across a vertical line with the specified position
+    /// Reflects the vector across a vertical line with the specified x position
     /// </summary>
     /// </summary>
     public VecI ReflectX(int lineX)
     public VecI ReflectX(int lineX)
     {
     {
         return new(2 * lineX - X, Y);
         return new(2 * lineX - X, Y);
     }
     }
     /// <summary>
     /// <summary>
-    /// Reflects the vector along a horizontal line with the specified position
+    /// Reflects the vector across a horizontal line with the specified y position
     /// </summary>
     /// </summary>
     public VecI ReflectY(int lineY)
     public VecI ReflectY(int lineY)
     {
     {
@@ -77,6 +79,14 @@ public struct VecI
     {
     {
         return new VecD(a.X / b, a.Y / b);
         return new VecD(a.X / b, a.Y / b);
     }
     }
+    public static VecI operator %(VecI a, int b)
+    {
+        return new(a.X % b, a.Y % b);
+    }
+    public static VecD operator %(VecI a, double b)
+    {
+        return new(a.X % b, a.Y % b);
+    }
     public static bool operator ==(VecI a, VecI b)
     public static bool operator ==(VecI a, VecI b)
     {
     {
         return a.X == b.X && a.Y == b.Y;
         return a.X == b.X && a.Y == b.Y;

+ 106 - 0
src/ChunkyImageLib/Operations/ChunkyImageOperation.cs

@@ -0,0 +1,106 @@
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
+
+namespace ChunkyImageLib.Operations;
+internal class ChunkyImageOperation : IDrawOperation
+{
+    private readonly ChunkyImage imageToDraw;
+    private readonly VecI pos;
+    private readonly bool mirrorHorizontal;
+    private readonly bool mirrorVertical;
+
+    public bool IgnoreEmptyChunks => false;
+
+    public ChunkyImageOperation(ChunkyImage imageToDraw, VecI pos, bool mirrorHorizontal, bool mirrorVertical)
+    {
+        this.imageToDraw = imageToDraw;
+        this.pos = pos;
+        this.mirrorHorizontal = mirrorHorizontal;
+        this.mirrorVertical = mirrorVertical;
+    }
+
+    public void DrawOnChunk(Chunk chunk, VecI chunkPos)
+    {
+        chunk.Surface.SkiaSurface.Canvas.Save();
+
+        {
+            VecI pixelPos = chunkPos * ChunkyImage.FullChunkSize;
+            VecI topLeft = GetTopLeft();
+            SKRect clippingRect = SKRect.Create(
+                OperationHelper.ConvertForResolution(topLeft - pixelPos, chunk.Resolution),
+                OperationHelper.ConvertForResolution(imageToDraw.CommittedSize, chunk.Resolution));
+            chunk.Surface.SkiaSurface.Canvas.ClipRect(clippingRect);
+        }
+
+        if (mirrorHorizontal)
+        {
+            chunkPos.X = (-((chunkPos.X * ChunkyImage.FullChunkSize) - pos.X) + pos.X) / ChunkyImage.FullChunkSize - 1;
+            chunk.Surface.SkiaSurface.Canvas.Translate(chunk.PixelSize.X, 0);
+            chunk.Surface.SkiaSurface.Canvas.Scale(-1, 0);
+        }
+        if (mirrorVertical)
+        {
+            chunkPos.Y = (-((chunkPos.Y * ChunkyImage.FullChunkSize) - pos.Y) + pos.Y) / ChunkyImage.FullChunkSize - 1;
+            chunk.Surface.SkiaSurface.Canvas.Translate(0, chunk.PixelSize.Y);
+            chunk.Surface.SkiaSurface.Canvas.Scale(0, -1);
+        }
+
+        VecD posOnImage = chunkPos - (pos / (double)ChunkyImage.FullChunkSize);
+        int topY = (int)Math.Floor(posOnImage.Y);
+        int bottomY = (int)Math.Ceiling(posOnImage.Y);
+        int leftX = (int)Math.Floor(posOnImage.X);
+        int rightX = (int)Math.Ceiling(posOnImage.X);
+
+        // this is kinda dumb
+        if (pos % ChunkyImage.FullChunkSize == VecI.Zero)
+        {
+            imageToDraw.DrawCommittedChunkOn((VecI)posOnImage, chunk.Resolution, chunk.Surface.SkiaSurface, VecI.Zero);
+        }
+        else if (pos.X % ChunkyImage.FullChunkSize == 0)
+        {
+            imageToDraw.DrawCommittedChunkOn(new VecI((int)posOnImage.X, topY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI(0, (int)(topY - posOnImage.Y)));
+            imageToDraw.DrawCommittedChunkOn(new VecI((int)posOnImage.X, bottomY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI(0, (int)(bottomY - posOnImage.Y)));
+        }
+        else if (pos.Y % ChunkyImage.FullChunkSize == 0)
+        {
+            imageToDraw.DrawCommittedChunkOn(new VecI(leftX, (int)posOnImage.Y), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(leftX - posOnImage.X), 0));
+            imageToDraw.DrawCommittedChunkOn(new VecI(rightX, (int)posOnImage.Y), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(rightX - posOnImage.X), 0));
+        }
+        else
+        {
+            imageToDraw.DrawCommittedChunkOn(new VecI(leftX, topY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(leftX - posOnImage.X), (int)(topY - posOnImage.Y)));
+            imageToDraw.DrawCommittedChunkOn(new VecI(rightX, topY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(rightX - posOnImage.X), (int)(topY - posOnImage.Y)));
+            imageToDraw.DrawCommittedChunkOn(new VecI(leftX, bottomY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(leftX - posOnImage.X), (int)(bottomY - posOnImage.Y)));
+            imageToDraw.DrawCommittedChunkOn(new VecI(rightX, bottomY), chunk.Resolution, chunk.Surface.SkiaSurface, new VecI((int)(rightX - posOnImage.X), (int)(bottomY - posOnImage.Y)));
+        }
+
+        chunk.Surface.SkiaSurface.Canvas.Restore();
+    }
+
+    public HashSet<VecI> FindAffectedChunks()
+    {
+        return OperationHelper.FindChunksFullyInsideRectangle(GetTopLeft(), imageToDraw.CommittedSize, ChunkyImage.FullChunkSize);
+    }
+
+    private VecI GetTopLeft()
+    {
+        VecI topLeft = pos;
+        if (mirrorHorizontal)
+            topLeft.X -= imageToDraw.LatestSize.X;
+        if (mirrorVertical)
+            topLeft.Y -= imageToDraw.LatestSize.Y;
+        return topLeft;
+    }
+
+    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    {
+        var newPos = pos;
+        if (verAxisX is not null)
+            newPos = newPos.ReflectX((int)verAxisX);
+        if (horAxisY is not null)
+            newPos = newPos.ReflectY((int)horAxisY);
+        return new ChunkyImageOperation(imageToDraw, newPos, mirrorHorizontal ^ (verAxisX is not null), mirrorVertical ^ (horAxisY is not null));
+    }
+
+    public void Dispose() { }
+}

+ 2 - 0
src/ChunkyImageLib/Operations/OperationHelper.cs

@@ -77,6 +77,8 @@ public static class OperationHelper
 
 
     public static SKMatrix CreateMatrixFromPoints(ShapeCorners corners, VecD size)
     public static SKMatrix CreateMatrixFromPoints(ShapeCorners corners, VecD size)
         => CreateMatrixFromPoints((SKPoint)corners.TopLeft, (SKPoint)corners.TopRight, (SKPoint)corners.BottomRight, (SKPoint)corners.BottomLeft, (float)size.X, (float)size.Y);
         => CreateMatrixFromPoints((SKPoint)corners.TopLeft, (SKPoint)corners.TopRight, (SKPoint)corners.BottomRight, (SKPoint)corners.BottomLeft, (float)size.X, (float)size.Y);
+
+    // see https://stackoverflow.com/questions/48416118/perspective-transform-in-skia/72364829#72364829
     public static SKMatrix CreateMatrixFromPoints(SKPoint topLeft, SKPoint topRight, SKPoint botRight, SKPoint botLeft, float width, float height)
     public static SKMatrix CreateMatrixFromPoints(SKPoint topLeft, SKPoint topRight, SKPoint botRight, SKPoint botLeft, float width, float height)
     {
     {
         (float x1, float y1) = (topLeft.X, topLeft.Y);
         (float x1, float y1) = (topLeft.X, topLeft.Y);

+ 2 - 1
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -17,7 +17,6 @@ internal class RectangleOperation : IDrawOperation
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     {
     {
         var skiaSurf = chunk.Surface.SkiaSurface;
         var skiaSurf = chunk.Surface.SkiaSurface;
-        // use a clipping rectangle with 2x stroke width to make sure stroke doesn't stick outside rect bounds
         skiaSurf.Canvas.Save();
         skiaSurf.Canvas.Save();
 
 
         var convertedPos = OperationHelper.ConvertForResolution(-Data.Size.Abs() / 2, chunk.Resolution);
         var convertedPos = OperationHelper.ConvertForResolution(-Data.Size.Abs() / 2, chunk.Resolution);
@@ -30,6 +29,8 @@ internal class RectangleOperation : IDrawOperation
 
 
         skiaSurf.Canvas.Translate((SKPoint)convertedCenter);
         skiaSurf.Canvas.Translate((SKPoint)convertedCenter);
         skiaSurf.Canvas.RotateRadians((float)Data.Angle);
         skiaSurf.Canvas.RotateRadians((float)Data.Angle);
+
+        // use a clipping rectangle with 2x stroke width to make sure stroke doesn't stick outside rect bounds
         skiaSurf.Canvas.ClipRect(rect);
         skiaSurf.Canvas.ClipRect(rect);
 
 
         // draw fill
         // draw fill

+ 9 - 8
src/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -1,4 +1,5 @@
 using ChunkyImageLib;
 using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
 using SkiaSharp;
 using SkiaSharp;
 using Xunit;
 using Xunit;
 
 
@@ -8,24 +9,24 @@ public class ChunkyImageTests
     [Fact]
     [Fact]
     public void ChunkyImage_Dispose_ReturnsAllChunks()
     public void ChunkyImage_Dispose_ReturnsAllChunks()
     {
     {
-        ChunkyImage image = new ChunkyImage(new(ChunkyImage.ChunkSize, ChunkyImage.ChunkSize));
+        ChunkyImage image = new ChunkyImage(new(ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
         image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 2, SKColors.AliceBlue, SKColors.Snow));
         image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 2, SKColors.AliceBlue, SKColors.Snow));
         using (Chunk target = Chunk.Create())
         using (Chunk target = Chunk.Create())
         {
         {
-            image.DrawMostUpToDateChunkOn(new(0, 0), ChunkyImageLib.DataHolders.ChunkResolution.Full, target.Surface.SkiaSurface, new(0, 0));
+            image.DrawMostUpToDateChunkOn(new(0, 0), ChunkyImageLib.DataHolders.ChunkResolution.Full, target.Surface.SkiaSurface, VecI.Zero);
             image.CancelChanges();
             image.CancelChanges();
-            image.EnqueueResize(new(ChunkyImage.ChunkSize * 4, ChunkyImage.ChunkSize * 4));
-            image.EnqueueDrawRectangle(new(new(0, 0), image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow, SKBlendMode.Multiply));
+            image.EnqueueResize(new(ChunkyImage.FullChunkSize * 4, ChunkyImage.FullChunkSize * 4));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow, SKBlendMode.Multiply));
             image.CommitChanges();
             image.CommitChanges();
             image.SetBlendMode(SKBlendMode.Overlay);
             image.SetBlendMode(SKBlendMode.Overlay);
-            image.EnqueueDrawRectangle(new(new(0, 0), image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow, SKBlendMode.Multiply));
-            image.EnqueueDrawRectangle(new(new(0, 0), image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow, SKBlendMode.Multiply));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
             image.CommitChanges();
             image.CommitChanges();
             image.SetBlendMode(SKBlendMode.Screen);
             image.SetBlendMode(SKBlendMode.Screen);
-            image.EnqueueDrawRectangle(new(new(0, 0), image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
             image.CancelChanges();
             image.CancelChanges();
             image.SetBlendMode(SKBlendMode.SrcOver);
             image.SetBlendMode(SKBlendMode.SrcOver);
-            image.EnqueueDrawRectangle(new(new(0, 0), image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, SKColors.AliceBlue, SKColors.Snow));
         }
         }
         image.Dispose();
         image.Dispose();
 
 

+ 3 - 8
src/PixiEditor.ChangeableDocument/Changes/Change.cs

@@ -1,15 +1,10 @@
-using OneOf;
-using OneOf.Types;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-
-namespace PixiEditor.ChangeableDocument.Changes;
+namespace PixiEditor.ChangeableDocument.Changes;
 
 
 internal abstract class Change : IDisposable
 internal abstract class Change : IDisposable
 {
 {
     public virtual bool IsMergeableWith(Change other) => false;
     public virtual bool IsMergeableWith(Change other) => false;
     public abstract OneOf<Success, Error> InitializeAndValidate(Document target);
     public abstract OneOf<Success, Error> InitializeAndValidate(Document target);
-    public abstract IChangeInfo? Apply(Document target, out bool ignoreInUndo);
-    public abstract IChangeInfo? Revert(Document target);
+    public abstract OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo);
+    public abstract OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target);
     public virtual void Dispose() { }
     public virtual void Dispose() { }
 };
 };

+ 87 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs

@@ -0,0 +1,87 @@
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+internal class ApplyLayerMask_Change : Change
+{
+    private readonly Guid layerGuid;
+    private CommittedChunkStorage? savedMask;
+    private CommittedChunkStorage? savedLayer;
+
+    [GenerateMakeChangeAction]
+    public ApplyLayerMask_Change(Guid layerGuid)
+    {
+        this.layerGuid = layerGuid;
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        var member = target.FindMember(layerGuid);
+        if (member is not Layer layer || layer.Mask is null)
+            return new Error();
+
+        savedLayer = new CommittedChunkStorage(layer.LayerImage, layer.LayerImage.FindCommittedChunks());
+        savedMask = new CommittedChunkStorage(layer.Mask, layer.Mask.FindCommittedChunks());
+        return new Success();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
+    {
+        var layer = (Layer)target.FindMemberOrThrow(layerGuid);
+        if (layer.Mask is null)
+            throw new InvalidOperationException("Cannot apply layer mask, no mask");
+
+        ChunkyImage newLayerImage = new ChunkyImage(target.Size);
+        newLayerImage.AddRasterClip(layer.Mask);
+        newLayerImage.EnqueueDrawChunkyImage(VecI.Zero, layer.LayerImage);
+        newLayerImage.CommitChanges();
+
+        var affectedChunks = layer.LayerImage.FindAllChunks();
+        // use a temp value to ensure that LayerImage always stays in a valid state
+        var toDispose = layer.LayerImage;
+        layer.LayerImage = newLayerImage;
+        toDispose.Dispose();
+
+        var toDisposeMask = layer.Mask;
+        layer.Mask = null;
+        toDisposeMask.Dispose();
+
+        ignoreInUndo = false;
+        return new List<IChangeInfo>
+        {
+            new StructureMemberMask_ChangeInfo() { GuidValue = layerGuid },
+            new LayerImageChunks_ChangeInfo() { GuidValue = layerGuid, Chunks = affectedChunks }
+        };
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var layer = (Layer)target.FindMemberOrThrow(layerGuid);
+        if (layer.Mask is not null)
+            throw new InvalidOperationException("Cannot restore layer mask, it already has one");
+        if (savedLayer is null || savedMask is null)
+            throw new InvalidOperationException("Cannot restore layer mask, no saved data");
+
+        ChunkyImage newMask = new ChunkyImage(target.Size);
+        savedMask.ApplyChunksToImage(newMask);
+        var affectedChunksMask = newMask.FindAffectedChunks();
+        newMask.CommitChanges();
+        layer.Mask = newMask;
+
+        savedLayer.ApplyChunksToImage(layer.LayerImage);
+        var affectedChunksLayer = layer.LayerImage.FindAffectedChunks();
+        layer.LayerImage.CommitChanges();
+
+        return new List<IChangeInfo>
+        {
+            new StructureMemberMask_ChangeInfo() { GuidValue = layerGuid },
+            new LayerImageChunks_ChangeInfo() { GuidValue = layerGuid, Chunks = affectedChunksLayer },
+            new MaskChunks_ChangeInfo() { GuidValue = layerGuid, Chunks = affectedChunksMask }
+        };
+    }
+
+    public override void Dispose()
+    {
+        savedLayer?.Dispose();
+        savedMask?.Dispose();
+    }
+}

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs

@@ -20,7 +20,7 @@ internal class ClearSelection_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         target.Selection.IsEmptyAndInactive = true;
         target.Selection.IsEmptyAndInactive = true;
 
 
@@ -36,7 +36,7 @@ internal class ClearSelection_Change : Change
         return new Selection_ChangeInfo() { Chunks = affChunks };
         return new Selection_ChangeInfo() { Chunks = affChunks };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         target.Selection.IsEmptyAndInactive = false;
         target.Selection.IsEmptyAndInactive = false;
 
 

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

@@ -47,7 +47,7 @@ internal class CombineStructureMembersOnto_Change : Change
         }
         }
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         Layer toDrawOn = (Layer)target.FindMemberOrThrow(targetLayer);
         Layer toDrawOn = (Layer)target.FindMemberOrThrow(targetLayer);
 
 
@@ -64,7 +64,7 @@ internal class CombineStructureMembersOnto_Change : Change
             OneOf<Chunk, EmptyChunk> combined = ChunkRenderer.MergeChosenMembers(chunk, ChunkResolution.Full, target.StructureRoot, layersToCombine);
             OneOf<Chunk, EmptyChunk> combined = ChunkRenderer.MergeChosenMembers(chunk, ChunkResolution.Full, target.StructureRoot, layersToCombine);
             if (combined.IsT0)
             if (combined.IsT0)
             {
             {
-                toDrawOn.LayerImage.EnqueueDrawImage(chunk * ChunkyImage.ChunkSize, combined.AsT0.Surface);
+                toDrawOn.LayerImage.EnqueueDrawImage(chunk * ChunkyImage.FullChunkSize, combined.AsT0.Surface);
                 combined.AsT0.Surface.Dispose();
                 combined.AsT0.Surface.Dispose();
             }
             }
         }
         }
@@ -80,7 +80,7 @@ internal class CombineStructureMembersOnto_Change : Change
         };
         };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         Layer toDrawOn = (Layer)target.FindMemberOrThrow(targetLayer);
         Layer toDrawOn = (Layer)target.FindMemberOrThrow(targetLayer);
 
 

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

@@ -42,14 +42,14 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
         return affectedChunks;
         return affectedChunks;
     }
     }
 
 
-    public override 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);
         var chunks = UpdateRectangle(target, targetImage);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var affectedChunks = UpdateRectangle(target, targetImage);
         var affectedChunks = UpdateRectangle(target, targetImage);
@@ -60,14 +60,14 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         storedChunks!.ApplyChunksToImage(targetImage);
         storedChunks!.ApplyChunksToImage(targetImage);
         storedChunks.Dispose();
         storedChunks.Dispose();
         storedChunks = null;
         storedChunks = null;
 
 
-        IChangeInfo changes = DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, targetImage.FindAffectedChunks(), drawOnMask);
+        var changes = DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, targetImage.FindAffectedChunks(), drawOnMask);
         targetImage.CommitChanges();
         targetImage.CommitChanges();
         return changes;
         return changes;
     }
     }

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

@@ -46,7 +46,7 @@ internal static class DrawingChangeHelper
         return true;
         return true;
     }
     }
 
 
-    public static IChangeInfo CreateChunkChangeInfo(Guid memberGuid, HashSet<VecI> affectedChunks, bool drawOnMask)
+    public static OneOf<None, IChangeInfo, List<IChangeInfo>> CreateChunkChangeInfo(Guid memberGuid, HashSet<VecI> affectedChunks, bool drawOnMask)
     {
     {
         return drawOnMask switch
         return drawOnMask switch
         {
         {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillChunkStorage.cs

@@ -22,7 +22,7 @@ internal class FloodFillChunkStorage : IDisposable
         if (acquiredChunks.ContainsKey(pos))
         if (acquiredChunks.ContainsKey(pos))
             return acquiredChunks[pos];
             return acquiredChunks[pos];
         Chunk chunk = Chunk.Create(ChunkResolution.Full);
         Chunk chunk = Chunk.Create(ChunkResolution.Full);
-        if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+        if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunk.Surface.SkiaSurface, VecI.Zero, ReplacingPaint))
             chunk.Surface.SkiaSurface.Canvas.Clear();
             chunk.Surface.SkiaSurface.Canvas.Clear();
         acquiredChunks[pos] = chunk;
         acquiredChunks[pos] = chunk;
         return chunk;
         return chunk;

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

@@ -27,7 +27,7 @@ internal class FloodFill_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
 
 
@@ -38,7 +38,7 @@ internal class FloodFill_Change : Change
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         if (chunkStorage is null)
         if (chunkStorage is null)
             throw new InvalidOperationException("No saved chunks to revert to");
             throw new InvalidOperationException("No saved chunks to revert to");

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

@@ -45,7 +45,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         return affectedChunks;
         return affectedChunks;
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var chunks = DrawImage(target, targetImage);
         var chunks = DrawImage(target, targetImage);
@@ -57,13 +57,13 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
     }
     }
 
 
-    public override 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.CreateChunkChangeInfo(memberGuid, DrawImage(target, targetImage), drawOnMask);
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         if (savedChunks is null)
         if (savedChunks is null)
             throw new InvalidOperationException("No saved chunks to restore");
             throw new InvalidOperationException("No saved chunks to restore");

+ 12 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs

@@ -30,7 +30,7 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
         this.size = size;
         this.size = size;
     }
     }
 
 
-    public override IChangeInfo? ApplyTemporarily(Document target)
+    private (Selection_ChangeInfo info, HashSet<VecI> chunks) CommonApply(Document target)
     {
     {
         var oldChunks = target.Selection.SelectionImage.FindAffectedChunks();
         var oldChunks = target.Selection.SelectionImage.FindAffectedChunks();
         target.Selection.SelectionImage.CancelChanges();
         target.Selection.SelectionImage.CancelChanges();
@@ -48,20 +48,26 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
         target.Selection.SelectionPath = originalPath!.Op(rect, SKPathOp.Union);
         target.Selection.SelectionPath = originalPath!.Op(rect, SKPathOp.Union);
 
 
         oldChunks.UnionWith(target.Selection.SelectionImage.FindAffectedChunks());
         oldChunks.UnionWith(target.Selection.SelectionImage.FindAffectedChunks());
-        return new Selection_ChangeInfo() { Chunks = oldChunks };
+        return (new Selection_ChangeInfo() { Chunks = oldChunks }, oldChunks);
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
-        var changes = ApplyTemporarily(target);
-        originalSelectionState = new CommittedChunkStorage(target.Selection.SelectionImage, ((Selection_ChangeInfo)changes!).Chunks!);
+        var (info, _) = CommonApply(target);
+        return info;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
+    {
+        var (changes, affChunks) = CommonApply(target);
+        originalSelectionState = new CommittedChunkStorage(target.Selection.SelectionImage, affChunks);
         target.Selection.SelectionImage.CommitChanges();
         target.Selection.SelectionImage.CommitChanges();
         target.Selection.IsEmptyAndInactive = target.Selection.SelectionImage.CheckIfCommittedIsEmpty();
         target.Selection.IsEmptyAndInactive = target.Selection.SelectionImage.CheckIfCommittedIsEmpty();
         ignoreInUndo = false;
         ignoreInUndo = false;
         return changes;
         return changes;
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         target.Selection.IsEmptyAndInactive = originalIsEmpty;
         target.Selection.IsEmptyAndInactive = originalIsEmpty;
         originalSelectionState!.ApplyChunksToImage(target.Selection.SelectionImage);
         originalSelectionState!.ApplyChunksToImage(target.Selection.SelectionImage);

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs

@@ -20,7 +20,7 @@ internal class CreateStructureMemberMask_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         var member = target.FindMemberOrThrow(targetMember);
         var member = target.FindMemberOrThrow(targetMember);
         if (member.Mask is not null)
         if (member.Mask is not null)
@@ -31,7 +31,7 @@ internal class CreateStructureMemberMask_Change : Change
         return new StructureMemberMask_ChangeInfo() { GuidValue = targetMember };
         return new StructureMemberMask_ChangeInfo() { GuidValue = targetMember };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var member = target.FindMemberOrThrow(targetMember);
         var member = target.FindMemberOrThrow(targetMember);
         if (member.Mask is null)
         if (member.Mask is null)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs

@@ -22,7 +22,7 @@ internal class DeleteStructureMemberMask_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         var member = target.FindMemberOrThrow(memberGuid);
         var member = target.FindMemberOrThrow(memberGuid);
         if (member.Mask is null)
         if (member.Mask is null)
@@ -34,7 +34,7 @@ internal class DeleteStructureMemberMask_Change : Change
         return new StructureMemberMask_ChangeInfo() { GuidValue = memberGuid };
         return new StructureMemberMask_ChangeInfo() { GuidValue = memberGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var member = target.FindMemberOrThrow(memberGuid);
         var member = target.FindMemberOrThrow(memberGuid);
         if (member.Mask is not null)
         if (member.Mask is not null)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerLockTransparency_Change.cs

@@ -25,14 +25,14 @@ internal class LayerLockTransparency_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency = newValue;
         ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency = newValue;
         ignoreInUndo = false;
         ignoreInUndo = false;
         return new LayerLockTransparency_ChangeInfo() { GuidValue = layerGuid };
         return new LayerLockTransparency_ChangeInfo() { GuidValue = layerGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency = originalValue;
         ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency = originalValue;
         return new LayerLockTransparency_ChangeInfo() { GuidValue = layerGuid };
         return new LayerLockTransparency_ChangeInfo() { GuidValue = layerGuid };

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberBlendMode_Change.cs

@@ -24,7 +24,7 @@ internal class StructureMemberBlendMode_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         var member = target.FindMemberOrThrow(targetGuid);
         var member = target.FindMemberOrThrow(targetGuid);
         member.BlendMode = newBlendMode;
         member.BlendMode = newBlendMode;
@@ -32,7 +32,7 @@ internal class StructureMemberBlendMode_Change : Change
         return new StructureMemberBlendMode_ChangeInfo() { GuidValue = targetGuid };
         return new StructureMemberBlendMode_ChangeInfo() { GuidValue = targetGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         var member = target.FindMemberOrThrow(targetGuid);
         var member = target.FindMemberOrThrow(targetGuid);
         member.BlendMode = originalBlendMode;
         member.BlendMode = originalBlendMode;

+ 2 - 9
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberClipToMemberBelow_Change.cs

@@ -23,23 +23,16 @@ internal class StructureMemberClipToMemberBelow_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
-        if (originalValue == newValue)
-        {
-            ignoreInUndo = true;
-            return null;
-        }
         var member = target.FindMemberOrThrow(memberGuid);
         var member = target.FindMemberOrThrow(memberGuid);
         member.ClipToMemberBelow = newValue;
         member.ClipToMemberBelow = newValue;
         ignoreInUndo = false;
         ignoreInUndo = false;
         return new StructureMemberClipToMemberBelow_ChangeInfo() { GuidValue = memberGuid };
         return new StructureMemberClipToMemberBelow_ChangeInfo() { GuidValue = memberGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
-        if (originalValue == newValue)
-            return null;
         var member = target.FindMemberOrThrow(memberGuid);
         var member = target.FindMemberOrThrow(memberGuid);
         member.ClipToMemberBelow = originalValue;
         member.ClipToMemberBelow = originalValue;
         return new StructureMemberClipToMemberBelow_ChangeInfo() { GuidValue = memberGuid };
         return new StructureMemberClipToMemberBelow_ChangeInfo() { GuidValue = memberGuid };

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs

@@ -23,7 +23,7 @@ internal class StructureMemberIsVisible_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         // don't record layer/folder visibility changes - it's just more convenient this way
         // don't record layer/folder visibility changes - it's just more convenient this way
         ignoreInUndo = true;
         ignoreInUndo = true;
@@ -31,7 +31,7 @@ internal class StructureMemberIsVisible_Change : Change
         return new StructureMemberIsVisible_ChangeInfo() { GuidValue = targetMember };
         return new StructureMemberIsVisible_ChangeInfo() { GuidValue = targetMember };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         target.FindMemberOrThrow(targetMember).IsVisible = originalIsVisible!.Value;
         target.FindMemberOrThrow(targetMember).IsVisible = originalIsVisible!.Value;
         return new StructureMemberIsVisible_ChangeInfo() { GuidValue = targetMember };
         return new StructureMemberIsVisible_ChangeInfo() { GuidValue = targetMember };

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs

@@ -24,7 +24,7 @@ internal class StructureMemberName_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         target.FindMemberOrThrow(targetMember).Name = newName;
         target.FindMemberOrThrow(targetMember).Name = newName;
 
 
@@ -32,7 +32,7 @@ internal class StructureMemberName_Change : Change
         return new StructureMemberName_ChangeInfo() { GuidValue = targetMember };
         return new StructureMemberName_ChangeInfo() { GuidValue = targetMember };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         if (originalName is null)
         if (originalName is null)
             throw new InvalidOperationException("No name to revert to");
             throw new InvalidOperationException("No name to revert to");

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs

@@ -31,14 +31,14 @@ internal class StructureMemberOpacity_UpdateableChange : UpdateableChange
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo? ApplyTemporarily(Document target) => Apply(target, out _);
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target) => Apply(target, out _);
 
 
-    public override IChangeInfo? Apply(Document document, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, out bool ignoreInUndo)
     {
     {
         if (originalOpacity == newOpacity)
         if (originalOpacity == newOpacity)
         {
         {
             ignoreInUndo = true;
             ignoreInUndo = true;
-            return null;
+            return new None();
         }
         }
 
 
         var member = document.FindMemberOrThrow(memberGuid);
         var member = document.FindMemberOrThrow(memberGuid);
@@ -48,10 +48,10 @@ internal class StructureMemberOpacity_UpdateableChange : UpdateableChange
         return new StructureMemberOpacity_ChangeInfo() { GuidValue = memberGuid };
         return new StructureMemberOpacity_ChangeInfo() { GuidValue = memberGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document document)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document document)
     {
     {
         if (originalOpacity == newOpacity)
         if (originalOpacity == newOpacity)
-            return null;
+            return new None();
 
 
         var member = document.FindMemberOrThrow(memberGuid);
         var member = document.FindMemberOrThrow(memberGuid);
         member.Opacity = originalOpacity;
         member.Opacity = originalOpacity;

+ 2 - 11
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -40,14 +40,8 @@ internal class ResizeCanvas_Change : Change
         }
         }
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
-        if (originalSize == newSize)
-        {
-            ignoreInUndo = true;
-            return null;
-        }
-
         target.Size = newSize;
         target.Size = newSize;
         target.VerticalSymmetryAxisX = Math.Clamp(originalVerAxisX, 0, target.Size.X);
         target.VerticalSymmetryAxisX = Math.Clamp(originalVerAxisX, 0, target.Size.X);
         target.HorizontalSymmetryAxisY = Math.Clamp(originalHorAxisY, 0, target.Size.Y);
         target.HorizontalSymmetryAxisY = Math.Clamp(originalHorAxisY, 0, target.Size.Y);
@@ -74,11 +68,8 @@ internal class ResizeCanvas_Change : Change
         return new Size_ChangeInfo();
         return new Size_ChangeInfo();
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
-        if (originalSize == newSize)
-            return null;
-
         target.Size = originalSize;
         target.Size = originalSize;
         ForEachLayer(target.StructureRoot, (layer) =>
         ForEachLayer(target.StructureRoot, (layer) =>
         {
         {

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

@@ -42,23 +42,23 @@ internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
             throw new NotImplementedException();
             throw new NotImplementedException();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         ignoreInUndo = originalPos == newPos;
         ignoreInUndo = originalPos == newPos;
         SetPosition(target, newPos);
         SetPosition(target, newPos);
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
     }
     }
 
 
-    public override IChangeInfo? ApplyTemporarily(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
         SetPosition(target, newPos);
         SetPosition(target, newPos);
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         if (originalPos == newPos)
         if (originalPos == newPos)
-            return null;
+            return new None();
         SetPosition(target, originalPos);
         SetPosition(target, originalPos);
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
         return new SymmetryAxisPosition_ChangeInfo() { Direction = direction };
     }
     }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisState_Change.cs

@@ -38,14 +38,14 @@ internal class SymmetryAxisState_Change : Change
             throw new NotImplementedException();
             throw new NotImplementedException();
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         SetState(target, newEnabled);
         SetState(target, newEnabled);
         ignoreInUndo = false;
         ignoreInUndo = false;
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         SetState(target, originalEnabled);
         SetState(target, originalEnabled);
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -27,7 +27,7 @@ internal class CreateStructureMember_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo Apply(Document document, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, out bool ignoreInUndo)
     {
     {
         var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
         var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
 
 
@@ -44,7 +44,7 @@ internal class CreateStructureMember_Change : Change
         return new CreateStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
         return new CreateStructureMember_ChangeInfo() { GuidValue = newMemberGuid };
     }
     }
 
 
-    public override IChangeInfo Revert(Document document)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document document)
     {
     {
         var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
         var folder = (Folder)document.FindMemberOrThrow(parentFolderGuid);
         var child = document.FindMemberOrThrow(newMemberGuid);
         var child = document.FindMemberOrThrow(newMemberGuid);

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs

@@ -27,7 +27,7 @@ internal class DeleteStructureMember_Change : Change
         return new Success();
         return new Success();
     }
     }
 
 
-    public override IChangeInfo Apply(Document document, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, out bool ignoreInUndo)
     {
     {
         var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
         var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
         parent.Children = parent.Children.Remove(member);
         parent.Children = parent.Children.Remove(member);
@@ -36,7 +36,7 @@ internal class DeleteStructureMember_Change : Change
         return new DeleteStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentGuid = parentGuid };
         return new DeleteStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentGuid = parentGuid };
     }
     }
 
 
-    public override IChangeInfo Revert(Document doc)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document doc)
     {
     {
         var parent = (Folder)doc.FindMemberOrThrow(parentGuid);
         var parent = (Folder)doc.FindMemberOrThrow(parentGuid);
 
 

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -40,14 +40,14 @@ internal class MoveStructureMember_Change : Change
         targetFolder.Children = targetFolder.Children.Insert(targetIndex, member);
         targetFolder.Children = targetFolder.Children.Insert(targetIndex, member);
     }
     }
 
 
-    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
     {
         Move(target, memberGuid, targetFolderGuid, targetFolderIndex);
         Move(target, memberGuid, targetFolderGuid, targetFolderIndex);
         ignoreInUndo = false;
         ignoreInUndo = false;
         return new MoveStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentFromGuid = originalFolderGuid, ParentToGuid = targetFolderGuid };
         return new MoveStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentFromGuid = originalFolderGuid, ParentToGuid = targetFolderGuid };
     }
     }
 
 
-    public override IChangeInfo? Revert(Document target)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
         Move(target, memberGuid, originalFolderGuid, originalFolderIndex);
         Move(target, memberGuid, originalFolderGuid, originalFolderIndex);
         return new MoveStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentFromGuid = targetFolderGuid, ParentToGuid = originalFolderGuid };
         return new MoveStructureMember_ChangeInfo() { GuidValue = memberGuid, ParentFromGuid = targetFolderGuid, ParentToGuid = originalFolderGuid };

+ 2 - 5
src/PixiEditor.ChangeableDocument/Changes/UpdateableChange.cs

@@ -1,9 +1,6 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-
-namespace PixiEditor.ChangeableDocument.Changes;
+namespace PixiEditor.ChangeableDocument.Changes;
 
 
 internal abstract class UpdateableChange : Change
 internal abstract class UpdateableChange : Change
 {
 {
-    public abstract IChangeInfo? ApplyTemporarily(Document target);
+    public abstract OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target);
 }
 }

+ 40 - 24
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -92,39 +92,49 @@ public class DocumentChangeTracker : IDisposable
         return true;
         return true;
     }
     }
 
 
-    private List<IChangeInfo?> Undo()
+    private List<IChangeInfo> Undo()
     {
     {
         if (undoStack.Count == 0)
         if (undoStack.Count == 0)
-            return new List<IChangeInfo?>();
+            return new List<IChangeInfo>();
         if (activePacket is not null || activeUpdateableChange is not null)
         if (activePacket is not null || activeUpdateableChange is not null)
         {
         {
             Trace.WriteLine("Attempted to undo while there is an active updateable change or an unfinished undo packet");
             Trace.WriteLine("Attempted to undo while there is an active updateable change or an unfinished undo packet");
-            return new List<IChangeInfo?>();
+            return new List<IChangeInfo>();
         }
         }
-        List<IChangeInfo?> changeInfos = new();
+        List<IChangeInfo> changeInfos = new();
         List<Change> changePacket = undoStack.Pop();
         List<Change> changePacket = undoStack.Pop();
 
 
         for (int i = changePacket.Count - 1; i >= 0; i--)
         for (int i = changePacket.Count - 1; i >= 0; i--)
-            changeInfos.Add(changePacket[i].Revert(document));
+        {
+            changePacket[i].Revert(document).Switch(
+                (None _) => { },
+                (IChangeInfo info) => changeInfos.Add(info),
+                (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
+        }
 
 
         redoStack.Push(changePacket);
         redoStack.Push(changePacket);
         return changeInfos;
         return changeInfos;
     }
     }
 
 
-    private List<IChangeInfo?> Redo()
+    private List<IChangeInfo> Redo()
     {
     {
         if (redoStack.Count == 0)
         if (redoStack.Count == 0)
-            return new List<IChangeInfo?>();
+            return new List<IChangeInfo>();
         if (activePacket is not null || activeUpdateableChange is not null)
         if (activePacket is not null || activeUpdateableChange is not null)
         {
         {
             Trace.WriteLine("Attempted to redo while there is an active updateable change or an unfinished undo packet");
             Trace.WriteLine("Attempted to redo while there is an active updateable change or an unfinished undo packet");
-            return new List<IChangeInfo?>();
+            return new List<IChangeInfo>();
         }
         }
-        List<IChangeInfo?> changeInfos = new();
+        List<IChangeInfo> changeInfos = new();
         List<Change> changePacket = redoStack.Pop();
         List<Change> changePacket = redoStack.Pop();
 
 
         for (int i = 0; i < changePacket.Count; i++)
         for (int i = 0; i < changePacket.Count; i++)
-            changeInfos.Add(changePacket[i].Apply(document, out _));
+        {
+            changePacket[i].Apply(document, out _).Switch(
+                (None _) => { },
+                (IChangeInfo info) => changeInfos.Add(info),
+                (List<IChangeInfo> infos) => changeInfos.AddRange(infos)); ;
+        }
 
 
         undoStack.Push(changePacket);
         undoStack.Push(changePacket);
         return changeInfos;
         return changeInfos;
@@ -147,12 +157,12 @@ public class DocumentChangeTracker : IDisposable
         undoStack.Clear();
         undoStack.Clear();
     }
     }
 
 
-    private IChangeInfo? ProcessMakeChangeAction(IMakeChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessMakeChangeAction(IMakeChangeAction act)
     {
     {
         if (activeUpdateableChange is not null)
         if (activeUpdateableChange is not null)
         {
         {
             Trace.WriteLine($"Attempted to execute make change action {act} while {activeUpdateableChange} is active");
             Trace.WriteLine($"Attempted to execute make change action {act} while {activeUpdateableChange} is active");
-            return null;
+            return new None();
         }
         }
         var change = act.CreateCorrespondingChange();
         var change = act.CreateCorrespondingChange();
         var validationResult = change.InitializeAndValidate(document);
         var validationResult = change.InitializeAndValidate(document);
@@ -160,7 +170,7 @@ public class DocumentChangeTracker : IDisposable
         {
         {
             Trace.WriteLine($"Change {change} failed validation");
             Trace.WriteLine($"Change {change} failed validation");
             change.Dispose();
             change.Dispose();
-            return null;
+            return new None();
         }
         }
 
 
         var info = change.Apply(document, out bool ignoreInUndo);
         var info = change.Apply(document, out bool ignoreInUndo);
@@ -171,7 +181,7 @@ public class DocumentChangeTracker : IDisposable
         return info;
         return info;
     }
     }
 
 
-    private IChangeInfo? ProcessStartOrUpdateChangeAction(IStartOrUpdateChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessStartOrUpdateChangeAction(IStartOrUpdateChangeAction act)
     {
     {
         if (activeUpdateableChange is null)
         if (activeUpdateableChange is null)
         {
         {
@@ -181,30 +191,30 @@ public class DocumentChangeTracker : IDisposable
             {
             {
                 Trace.WriteLine($"Change {newChange} failed validation");
                 Trace.WriteLine($"Change {newChange} failed validation");
                 newChange.Dispose();
                 newChange.Dispose();
-                return null;
+                return new None();
             }
             }
             activeUpdateableChange = newChange;
             activeUpdateableChange = newChange;
         }
         }
         else if (!act.IsChangeTypeMatching(activeUpdateableChange))
         else if (!act.IsChangeTypeMatching(activeUpdateableChange))
         {
         {
             Trace.WriteLine($"Tried to start or update a change using action {act} while a change of type {activeUpdateableChange} is active");
             Trace.WriteLine($"Tried to start or update a change using action {act} while a change of type {activeUpdateableChange} is active");
-            return null;
+            return new None();
         }
         }
         act.UpdateCorrespodingChange(activeUpdateableChange);
         act.UpdateCorrespodingChange(activeUpdateableChange);
         return activeUpdateableChange.ApplyTemporarily(document);
         return activeUpdateableChange.ApplyTemporarily(document);
     }
     }
 
 
-    private IChangeInfo? ProcessEndChangeAction(IEndChangeAction act)
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> ProcessEndChangeAction(IEndChangeAction act)
     {
     {
         if (activeUpdateableChange is null)
         if (activeUpdateableChange is null)
         {
         {
             Trace.WriteLine($"Attempted to end a change using action {act} while no changes are active");
             Trace.WriteLine($"Attempted to end a change using action {act} while no changes are active");
-            return null;
+            return new None();
         }
         }
         if (!act.IsChangeTypeMatching(activeUpdateableChange))
         if (!act.IsChangeTypeMatching(activeUpdateableChange))
         {
         {
             Trace.WriteLine($"Trying to end a change with an action {act} while change {activeUpdateableChange} is active");
             Trace.WriteLine($"Trying to end a change with an action {act} while change {activeUpdateableChange} is active");
-            return null;
+            return new None();
         }
         }
 
 
         var info = activeUpdateableChange.Apply(document, out bool ignoreInUndo);
         var info = activeUpdateableChange.Apply(document, out bool ignoreInUndo);
@@ -219,24 +229,30 @@ public class DocumentChangeTracker : IDisposable
     private List<IChangeInfo?> ProcessActionList(IReadOnlyList<IAction> actions)
     private List<IChangeInfo?> ProcessActionList(IReadOnlyList<IAction> actions)
     {
     {
         List<IChangeInfo?> changeInfos = new();
         List<IChangeInfo?> changeInfos = new();
+        void AddInfo(OneOf<None, IChangeInfo, List<IChangeInfo>> info) =>
+            info.Switch(
+                static (None _) => { },
+                (IChangeInfo info) => changeInfos.Add(info),
+                (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
+
         foreach (var action in actions)
         foreach (var action in actions)
         {
         {
             switch (action)
             switch (action)
             {
             {
                 case IMakeChangeAction act:
                 case IMakeChangeAction act:
-                    changeInfos.Add(ProcessMakeChangeAction(act));
+                    AddInfo(ProcessMakeChangeAction(act));
                     break;
                     break;
                 case IStartOrUpdateChangeAction act:
                 case IStartOrUpdateChangeAction act:
-                    changeInfos.Add(ProcessStartOrUpdateChangeAction(act));
+                    AddInfo(ProcessStartOrUpdateChangeAction(act));
                     break;
                     break;
                 case IEndChangeAction act:
                 case IEndChangeAction act:
-                    changeInfos.Add(ProcessEndChangeAction(act));
+                    AddInfo(ProcessEndChangeAction(act));
                     break;
                     break;
                 case Undo_Action act:
                 case Undo_Action act:
-                    changeInfos.AddRange(Undo());
+                    AddInfo(Undo());
                     break;
                     break;
                 case Redo_Action act:
                 case Redo_Action act:
-                    changeInfos.AddRange(Redo());
+                    AddInfo(Redo());
                     break;
                     break;
                 case ChangeBoundary_Action:
                 case ChangeBoundary_Action:
                     CompletePacket();
                     CompletePacket();

+ 1 - 0
src/PixiEditor.ChangeableDocument/GlobalUsings.cs

@@ -5,3 +5,4 @@ global using OneOf.Types;
 global using PixiEditor.ChangeableDocument.Actions.Attributes;
 global using PixiEditor.ChangeableDocument.Actions.Attributes;
 global using PixiEditor.ChangeableDocument.Changeables;
 global using PixiEditor.ChangeableDocument.Changeables;
 global using PixiEditor.ChangeableDocument.ChangeInfos;
 global using PixiEditor.ChangeableDocument.ChangeInfos;
+global using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;

+ 6 - 10
src/PixiEditor.ChangeableDocument/Rendering/ChunkRenderer.cs

@@ -1,8 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using ChunkyImageLib.Operations;
-using OneOf;
-using OneOf.Types;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using SkiaSharp;
 using SkiaSharp;
 
 
@@ -39,13 +35,13 @@ public static class ChunkRenderer
         context.UpdateFromMember(layer);
         context.UpdateFromMember(layer);
 
 
         Chunk renderingResult = Chunk.Create(resolution);
         Chunk renderingResult = Chunk.Create(resolution);
-        if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, new(0, 0), context.ReplacingPaintWithOpacity))
+        if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
         {
             renderingResult.Dispose();
             renderingResult.Dispose();
             return new EmptyChunk();
             return new EmptyChunk();
         }
         }
 
 
-        if (!layer.Mask!.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, new(0, 0), ClippingPaint))
+        if (!layer.Mask!.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, VecI.Zero, ClippingPaint))
         {
         {
             // should pretty much never happen due to the check above, but you can never be sure with many threads
             // should pretty much never happen due to the check above, but you can never be sure with many threads
             renderingResult.Dispose();
             renderingResult.Dispose();
@@ -70,7 +66,7 @@ public static class ChunkRenderer
 
 
         context.UpdateFromMember(layer);
         context.UpdateFromMember(layer);
         Chunk renderingResult = Chunk.Create(resolution);
         Chunk renderingResult = Chunk.Create(resolution);
-        if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, new(0, 0), context.ReplacingPaintWithOpacity))
+        if (!layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, renderingResult.Surface.SkiaSurface, VecI.Zero, context.ReplacingPaintWithOpacity))
         {
         {
             renderingResult.Dispose();
             renderingResult.Dispose();
             return new EmptyChunk();
             return new EmptyChunk();
@@ -103,7 +99,7 @@ public static class ChunkRenderer
             return;
             return;
         }
         }
         context.UpdateFromMember(layer);
         context.UpdateFromMember(layer);
-        layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, new(0, 0), context.BlendModeOpacityPaint);
+        layer.LayerImage.DrawMostUpToDateChunkOn(chunkPos, resolution, targetChunk.Surface.SkiaSurface, VecI.Zero, context.BlendModeOpacityPaint);
     }
     }
 
 
     private static OneOf<EmptyChunk, Chunk> RenderFolder(
     private static OneOf<EmptyChunk, Chunk> RenderFolder(
@@ -131,7 +127,7 @@ public static class ChunkRenderer
 
 
         if (folder.Mask is not null)
         if (folder.Mask is not null)
         {
         {
-            if (!folder.Mask.DrawMostUpToDateChunkOn(chunkPos, resolution, contents.Surface.SkiaSurface, new(0, 0), ClippingPaint))
+            if (!folder.Mask.DrawMostUpToDateChunkOn(chunkPos, resolution, contents.Surface.SkiaSurface, VecI.Zero, ClippingPaint))
             {
             {
                 // this shouldn't really happen due to the check above, but another thread could edit the mask in the meantime
                 // this shouldn't really happen due to the check above, but another thread could edit the mask in the meantime
                 contents.Dispose();
                 contents.Dispose();

+ 3 - 3
src/PixiEditor.Zoombox/Zoombox.xaml.cs

@@ -122,8 +122,8 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
         remove => RemoveHandler(ViewportMovedEvent, value);
         remove => RemoveHandler(ViewportMovedEvent, value);
     }
     }
 
 
-    public double CanvasX => ToScreenSpace(new(0, 0)).X;
-    public double CanvasY => ToScreenSpace(new(0, 0)).Y;
+    public double CanvasX => ToScreenSpace(VecD.Zero).X;
+    public double CanvasY => ToScreenSpace(VecD.Zero).Y;
 
 
     public double ScaleTransformXY => Scale;
     public double ScaleTransformXY => Scale;
     public double FlipTransformX => FlipX ? -1 : 1;
     public double FlipTransformX => FlipX ? -1 : 1;
@@ -356,7 +356,7 @@ public partial class Zoombox : ContentControl, INotifyPropertyChanged
     {
     {
         var zoombox = (Zoombox)obj;
         var zoombox = (Zoombox)obj;
 
 
-        VecD topLeft = zoombox.ToZoomboxSpace(new(0, 0)).Rotate(zoombox.Angle);
+        VecD topLeft = zoombox.ToZoomboxSpace(VecD.Zero).Rotate(zoombox.Angle);
         VecD bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.ActualWidth, zoombox.mainCanvas.ActualHeight)).Rotate(zoombox.Angle);
         VecD bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.ActualWidth, zoombox.mainCanvas.ActualHeight)).Rotate(zoombox.Angle);
 
 
         zoombox.Dimensions = (bottomRight - topLeft).Abs();
         zoombox.Dimensions = (bottomRight - topLeft).Abs();

+ 2 - 2
src/PixiEditorPrototype/Models/Rendering/AffectedChunkGatherer.cs

@@ -204,8 +204,8 @@ internal class AffectedChunkGatherer
     private void AddAllChunks(HashSet<VecI> chunks)
     private void AddAllChunks(HashSet<VecI> chunks)
     {
     {
         VecI size = new(
         VecI size = new(
-            (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.ChunkSize),
-            (int)Math.Ceiling(tracker.Document.Size.Y / (float)ChunkyImage.ChunkSize));
+            (int)Math.Ceiling(tracker.Document.Size.X / (float)ChunkyImage.FullChunkSize),
+            (int)Math.Ceiling(tracker.Document.Size.Y / (float)ChunkyImage.FullChunkSize));
         for (int i = 0; i < size.X; i++)
         for (int i = 0; i < size.X; i++)
         {
         {
             for (int j = 0; j < size.Y; j++)
             for (int j = 0; j < size.Y; j++)

+ 9 - 0
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -60,6 +60,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? ClearHistoryCommand { get; }
     public RelayCommand? ClearHistoryCommand { get; }
     public RelayCommand? CreateMaskCommand { get; }
     public RelayCommand? CreateMaskCommand { get; }
     public RelayCommand? DeleteMaskCommand { get; }
     public RelayCommand? DeleteMaskCommand { get; }
+    public RelayCommand? ApplyMaskCommand { get; }
     public RelayCommand? ToggleLockTransparencyCommand { get; }
     public RelayCommand? ToggleLockTransparencyCommand { get; }
     public RelayCommand? ApplyTransformCommand { get; }
     public RelayCommand? ApplyTransformCommand { get; }
     public RelayCommand? PasteImageCommand { get; }
     public RelayCommand? PasteImageCommand { get; }
@@ -119,6 +120,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
         DragSymmetryCommand = new RelayCommand(DragSymmetry);
         DragSymmetryCommand = new RelayCommand(DragSymmetry);
         EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
         EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
         ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
         ClipToMemberBelowCommand = new RelayCommand(ClipToMemberBelow);
+        ApplyMaskCommand = new RelayCommand(ApplyMask);
 
 
         foreach (var bitmap in Bitmaps)
         foreach (var bitmap in Bitmaps)
         {
         {
@@ -132,6 +134,13 @@ internal class DocumentViewModel : INotifyPropertyChanged
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
             (new CreateStructureMember_Action(StructureRoot.GuidValue, Guid.NewGuid(), 0, StructureMemberType.Layer));
     }
     }
 
 
+    private void ApplyMask(object? obj)
+    {
+        if (updateableChangeActive || SelectedStructureMember is not LayerViewModel layer || !layer.HasMask)
+            return;
+        Helpers.ActionAccumulator.AddFinishedActions(new ApplyLayerMask_Action(layer.GuidValue));
+    }
+
     private void ClipToMemberBelow(object? obj)
     private void ClipToMemberBelow(object? obj)
     {
     {
         if (updateableChangeActive || SelectedStructureMember is null)
         if (updateableChangeActive || SelectedStructureMember is null)

+ 3 - 0
src/PixiEditorPrototype/Views/MainWindow.xaml

@@ -35,6 +35,9 @@
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                     <Button Margin="5,0" Command="{Binding ActiveDocument.CreateMaskCommand}" Width="80">Create Mask</Button>
                     <Button Margin="5,0" Command="{Binding ActiveDocument.CreateMaskCommand}" Width="80">Create Mask</Button>
                     <Button Margin="5,0" Command="{Binding ActiveDocument.DeleteMaskCommand}" Width="80">Delete Mask</Button>
                     <Button Margin="5,0" Command="{Binding ActiveDocument.DeleteMaskCommand}" Width="80">Delete Mask</Button>
+                    <Button Margin="5,0" Command="{Binding ActiveDocument.ApplyMaskCommand}" Width="80">Apply Mask</Button>
+                </StackPanel>
+                <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                     <controls:BlendModeComboBox Margin="5,0" Width="80" SelectedBlendMode="{Binding ActiveDocument.SelectedStructureMember.BlendMode, Mode=TwoWay}"/>
                     <controls:BlendModeComboBox Margin="5,0" Width="80" SelectedBlendMode="{Binding ActiveDocument.SelectedStructureMember.BlendMode, Mode=TwoWay}"/>
                 </StackPanel>
                 </StackPanel>
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">
                 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,5,0,0">

+ 2 - 0
src/README.md

@@ -21,6 +21,7 @@ Decouples the state of a document from the UI.
         - [ ] Write chunks to the hard drive?
         - [ ] Write chunks to the hard drive?
         - [ ] Compress chunks?
         - [ ] Compress chunks?
     - [x] Linear color space for blending
     - [x] Linear color space for blending
+    - [ ] Make low res chunks use smooth filtering
     - [ ] Tests for everything related to the operation queueing
     - [ ] Tests for everything related to the operation queueing
     - Operations
     - Operations
         - [x] Support for paints with different blending (replace vs. alpha compose)
         - [x] Support for paints with different blending (replace vs. alpha compose)
@@ -107,6 +108,7 @@ Decouples the state of a document from the UI.
         - [x] Clip to selection
         - [x] Clip to selection
         - [x] Lock transparency
         - [x] Lock transparency
         - [x] Create/Delete mask
         - [x] Create/Delete mask
+        - [ ] Enable/Disable mask
         - [ ] Apply mask
         - [ ] Apply mask
 - ViewModel
 - ViewModel
     - [ ] Loading window when background thread is busy
     - [ ] Loading window when background thread is busy