Browse Source

Palettes, Swatches

Equbuxu 3 years ago
parent
commit
2230223149
26 changed files with 282 additions and 109 deletions
  1. 12 1
      src/ChunkyImageLib/ChunkyImage.cs
  2. 30 4
      src/ChunkyImageLib/DataHolders/ColorBounds.cs
  3. 1 1
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  4. 1 1
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  5. 1 1
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  6. 1 1
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  7. 1 1
      src/ChunkyImageLib/Operations/IDrawOperation.cs
  8. 1 1
      src/ChunkyImageLib/Operations/ImageOperation.cs
  9. 1 1
      src/ChunkyImageLib/Operations/PathOperation.cs
  10. 1 1
      src/ChunkyImageLib/Operations/PixelOperation.cs
  11. 1 1
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  12. 2 2
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  13. 61 0
      src/ChunkyImageLib/Operations/ReplaceColorOperation.cs
  14. 1 1
      src/ChunkyImageLib/Operations/SkiaLineOperation.cs
  15. 17 0
      src/ChunkyImageLib/SKColorEx.cs
  16. 3 0
      src/Custom.ruleset
  17. 7 3
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  18. 16 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  19. 9 37
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  20. 68 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ReplaceColor_Change.cs
  21. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs
  22. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  23. 11 1
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  24. 27 45
      src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs
  25. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs
  26. 6 4
      src/PixiEditor/Views/MainWindow.xaml

+ 12 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -401,6 +401,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueReplaceColor(SKColor oldColor, SKColor newColor)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            ReplaceColorOperation operation = new(oldColor, newColor);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawRectangle(ShapeData rect)
     {
@@ -571,7 +582,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
         foreach (var op in operations)
         {
-            var chunks = op.FindAffectedChunks();
+            var chunks = op.FindAffectedChunks(LatestSize);
             chunks.RemoveWhere(pos => IsOutsideBounds(pos, LatestSize));
             if (operation.IgnoreEmptyChunks)
                 chunks.IntersectWith(FindAllChunks());

+ 30 - 4
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillColorBounds.cs → src/ChunkyImageLib/DataHolders/ColorBounds.cs

@@ -1,7 +1,14 @@
-using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using SkiaSharp;
 
-namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
-internal struct FloodFillColorRange
+namespace ChunkyImageLib.DataHolders;
+
+public struct ColorBounds
 {
     public float LowerR { get; set; }
     public float LowerG { get; set; }
@@ -12,7 +19,7 @@ internal struct FloodFillColorRange
     public float UpperB { get; set; }
     public float UpperA { get; set; }
 
-    public FloodFillColorRange(SKColor color)
+    public ColorBounds(SKColor color)
     {
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         {
@@ -35,4 +42,23 @@ internal struct FloodFillColorRange
         (LowerB, UpperB) = FindInclusiveBoundaryPremul(color.Blue, a);
         (LowerA, UpperA) = FindInclusiveBoundary(color.Alpha);
     }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public unsafe bool IsWithinBounds(Half* pixel)
+    {
+        float r = (float)pixel[0];
+        float g = (float)pixel[1];
+        float b = (float)pixel[2];
+        float a = (float)pixel[3];
+        if (r < LowerR || r > UpperR)
+            return false;
+        if (g < LowerG || g > UpperG)
+            return false;
+        if (b < LowerB || b > UpperB)
+            return false;
+        if (a < LowerA || a > UpperA)
+            return false;
+        return true;
+    }
 }
+

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

@@ -35,7 +35,7 @@ internal class BresenhamLineOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         RectI bounds = RectI.FromTwoPoints(from, to + new VecI(1));
         return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);

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

@@ -93,7 +93,7 @@ internal class ChunkyImageOperation : IDrawOperation
         chunk.Surface.SkiaSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return OperationHelper.FindChunksTouchingRectangle(new(GetTopLeft(), imageToDraw.CommittedSize), ChunkyImage.FullChunkSize);
     }

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

@@ -25,7 +25,7 @@ internal class ClearRegionOperation : IDrawOperation
         chunk.Surface.SkiaSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return OperationHelper.FindChunksTouchingRectangle(rect, ChunkPool.FullChunkSize);
     }

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

@@ -89,7 +89,7 @@ internal class EllipseOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         var chunks = OperationHelper.FindChunksTouchingEllipse
             (location.Center, location.Width / 2.0, location.Height / 2.0, ChunkyImage.FullChunkSize);

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

@@ -6,6 +6,6 @@ internal interface IDrawOperation : IOperation
 {
     bool IgnoreEmptyChunks { get; }
     void DrawOnChunk(Chunk chunk, VecI chunkPos);
-    HashSet<VecI> FindAffectedChunks();
+    HashSet<VecI> FindAffectedChunks(VecI imageSize);
     IDrawOperation AsMirrored(int? verAxisX, int? horAxisY);
 }

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

@@ -68,7 +68,7 @@ internal class ImageOperation : IDrawOperation
         chunk.Surface.SkiaSurface.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return OperationHelper.FindChunksTouchingQuadrilateral(corners, ChunkPool.FullChunkSize);
     }

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

@@ -31,7 +31,7 @@ internal class PathOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
     }

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

@@ -32,7 +32,7 @@ internal class PixelOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) };
     }

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

@@ -32,7 +32,7 @@ internal class PixelsOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         return pixels.Select(static pixel => OperationHelper.GetChunkPos((VecI)pixel, ChunkyImage.FullChunkSize)).ToHashSet();
     }

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

@@ -49,9 +49,9 @@ internal class RectangleOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
-        if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 || Data.StrokeColor.Alpha == 0 && Data.FillColor.Alpha == 0)
+        if (Math.Abs(Data.Size.X) < 1 || Math.Abs(Data.Size.Y) < 1 || (Data.StrokeColor.Alpha == 0 && Data.FillColor.Alpha == 0))
             return new();
         if (Data.FillColor.Alpha != 0 || Math.Abs(Data.Size.X) == 1 || Math.Abs(Data.Size.Y) == 1)
             return OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle, ChunkPool.FullChunkSize);

+ 61 - 0
src/ChunkyImageLib/Operations/ReplaceColorOperation.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ChunkyImageLib.DataHolders;
+using SkiaSharp;
+
+namespace ChunkyImageLib.Operations;
+internal class ReplaceColorOperation : IDrawOperation
+{
+    private readonly SKColor oldColor;
+    private readonly SKColor newColor;
+
+    private readonly ColorBounds oldColorBounds;
+    private readonly ulong newColorBits;
+
+    public bool IgnoreEmptyChunks => true;
+
+    public ReplaceColorOperation(SKColor oldColor, SKColor newColor)
+    {
+        this.oldColor = oldColor;
+        this.newColor = newColor;
+        oldColorBounds = new ColorBounds(oldColor);
+        newColorBits = newColor.ToULong();
+    }
+
+    public void DrawOnChunk(Chunk chunk, VecI chunkPos)
+    {
+        ReplaceColor(oldColorBounds, newColorBits, chunk);
+    }
+
+    private static unsafe void ReplaceColor(ColorBounds oldColorBounds, ulong newColorBits, Chunk chunk)
+    {
+        int maxThreads = Environment.ProcessorCount;
+        VecI imageSize = chunk.PixelSize;
+        int rowsPerThread = imageSize.Y / maxThreads;
+
+        using SKPixmap pixmap = chunk.Surface.SkiaSurface.PeekPixels();
+        IntPtr pixels = pixmap.GetPixels();
+
+        Half* endOffset = (Half*)(pixels + pixmap.BytesSize);
+        for (Half* i = (Half*)pixels; i < endOffset; i += 4)
+        {
+            if (oldColorBounds.IsWithinBounds(i))
+                *(ulong*)i = newColorBits;
+        }
+    }
+
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
+    {
+        return OperationHelper.FindChunksTouchingRectangle(new RectI(VecI.Zero, imageSize), ChunkyImage.FullChunkSize);
+    }
+
+    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    {
+        return new ReplaceColorOperation(oldColor, newColor);
+    }
+
+    public void Dispose() { }
+}

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

@@ -35,7 +35,7 @@ internal class SkiaLineOperation : IDrawOperation
         surf.Canvas.Restore();
     }
 
-    public HashSet<VecI> FindAffectedChunks()
+    public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
         RectI bounds = RectI.FromTwoPoints(from, to).Inflate((int)Math.Ceiling(paint.StrokeWidth));
         return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);

+ 17 - 0
src/ChunkyImageLib/SKColorEx.cs

@@ -0,0 +1,17 @@
+using SkiaSharp;
+
+namespace ChunkyImageLib;
+public static class SKColorEx
+{
+    public static unsafe ulong ToULong(this SKColor color)
+    {
+        ulong result = 0;
+        Half* ptr = (Half*)&result;
+        float normalizedAlpha = color.Alpha / 255.0f;
+        ptr[0] = (Half)(color.Red / 255f * normalizedAlpha);
+        ptr[1] = (Half)(color.Green / 255f * normalizedAlpha);
+        ptr[2] = (Half)(color.Blue / 255f * normalizedAlpha);
+        ptr[3] = (Half)(normalizedAlpha);
+        return result;
+    }
+}

+ 3 - 0
src/Custom.ruleset

@@ -19,6 +19,7 @@
     <Rule Id="SA1005" Action="None" />
     <Rule Id="SA1008" Action="None" />
     <Rule Id="SA1009" Action="None" />
+    <Rule Id="SA1023" Action="None" />
     <Rule Id="SA1028" Action="None" />
     <Rule Id="SA1101" Action="None" />
     <Rule Id="SA1110" Action="None" />
@@ -28,6 +29,7 @@
     <Rule Id="SA1119" Action="None" />
     <Rule Id="SA1122" Action="None" />
     <Rule Id="SA1124" Action="None" />
+    <Rule Id="SA1127" Action="None" />
     <Rule Id="SA1128" Action="None" />
     <Rule Id="SA1129" Action="None" />
     <Rule Id="SA1130" Action="None" />
@@ -80,6 +82,7 @@
     <Rule Id="SA1605" Action="None" />
     <Rule Id="SA1606" Action="None" />
     <Rule Id="SA1607" Action="None" />
+    <Rule Id="SA1623" Action="None" />
     <Rule Id="SA1629" Action="None" />
     <Rule Id="SA1633" Action="None" />
     <Rule Id="SA1642" Action="None" />

+ 7 - 3
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -1,5 +1,7 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changeables;
 
@@ -14,10 +16,10 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     (IReadOnlyStructureMember, IReadOnlyFolder) IReadOnlyDocument.FindChildAndParentOrThrow(Guid guid) => FindChildAndParentOrThrow(guid);
 
     IReadOnlyReferenceLayer? IReadOnlyDocument.ReferenceLayer => ReferenceLayer;
+
     /// <summary>
     /// The default size for a new document
     /// </summary>
-
     public static VecI DefaultSize { get; } = new VecI(64, 64);
     internal Folder StructureRoot { get; } = new() { GuidValue = Guid.Empty };
     internal Selection Selection { get; } = new();
@@ -35,6 +37,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     }
     
     public void ForEveryReadonlyMember(Action<IReadOnlyStructureMember> action) => ForEveryReadonlyMember(StructureRoot, action);
+
     /// <summary>
     /// Performs the specified action on each member of the document
     /// </summary>
@@ -76,7 +79,8 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     /// </summary>
     /// <param name="guid">The <see cref="StructureMember.GuidValue"/> of the member</param>
     /// <returns>True if the member can be found and is of type <typeparamref name="T"/>, otherwise false</returns>
-    public bool HasMember<T>(Guid guid) where T : StructureMember
+    public bool HasMember<T>(Guid guid) 
+        where T : StructureMember
     {
         var list = FindMemberPath(guid);
         return list.Count > 0 && list[0] is T;

+ 16 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -1,4 +1,6 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
@@ -7,39 +9,48 @@ public interface IReadOnlyDocument
     /// The root folder of the document
     /// </summary>
     IReadOnlyFolder StructureRoot { get; }
+
     /// <summary>
     /// The selection of the document
     /// </summary>
     IReadOnlySelection Selection { get; }
+
     /// <summary>
     /// The size of the document
     /// </summary>
     VecI Size { get; }
+
     /// <summary>
     /// Is the horizontal symmetry axis enabled (Mirrors top and bottom)
     /// </summary>
     bool HorizontalSymmetryAxisEnabled { get; }
+
     /// <summary>
     /// Is the vertical symmetry axis enabled (Mirrors left and right)
     /// </summary>
     bool VerticalSymmetryAxisEnabled { get; }
+
     /// <summary>
     /// The position of the horizontal symmetry axis (Mirrors top and bottom)
     /// </summary>
     int HorizontalSymmetryAxisY { get; }
+
     /// <summary>
     /// The position of the vertical symmetry axis (Mirrors left and right)
     /// </summary>
     int VerticalSymmetryAxisX { get; }
+
     /// <summary>
     /// Performs the specified action on each readonly member of the document
     /// </summary>
     void ForEveryReadonlyMember(Action<IReadOnlyStructureMember> action);
+
     /// <summary>
     /// Finds the member with the <paramref name="guid"/> or returns null if not found
     /// </summary>
     /// <param name="guid">The <see cref="IReadOnlyStructureMember.GuidValue"/> of the member</param>
     IReadOnlyStructureMember? FindMember(Guid guid);
+
     /// <summary>
     /// Tries finding the member with the <paramref name="guid"/> of type <typeparamref name="T"/> and returns true if it was found
     /// </summary>
@@ -47,6 +58,7 @@ public interface IReadOnlyDocument
     /// <param name="member">The member</param>
     /// <returns>True if the member could be found, otherwise false</returns>
     bool TryFindMember<T>(Guid guid, [NotNullWhen(true)] out T? member) where T : IReadOnlyStructureMember;
+
     /// <summary>
     /// Tries finding the member with the <paramref name="guid"/> and returns true if it was found
     /// </summary>
@@ -54,12 +66,14 @@ public interface IReadOnlyDocument
     /// <param name="member">The member</param>
     /// <returns>True if the member could be found, otherwise false</returns>
     bool TryFindMember(Guid guid, [NotNullWhen(true)] out IReadOnlyStructureMember? member);
+
     /// <summary>
     /// Finds the member with the <paramref name="guid"/> or throws a ArgumentException if not found
     /// </summary>
     /// <param name="guid">The <see cref="StructureMember.GuidValue"/> of the member</param>
     /// <exception cref="ArgumentException">Thrown if the member could not be found</exception>
     IReadOnlyStructureMember FindMemberOrThrow(Guid guid);
+
     /// <summary>
     /// Finds a member with the <paramref name="childGuid"/>  and its parent, throws a ArgumentException if they can't be found
     /// </summary>
@@ -67,6 +81,7 @@ public interface IReadOnlyDocument
     /// <returns>A value tuple consisting of child (<see cref="ValueTuple{T, T}.Item1"/>) and parent (<see cref="ValueTuple{T, T}.Item2"/>)</returns>
     /// <exception cref="ArgumentException">Thrown if the member and parent could not be found</exception>
     (IReadOnlyStructureMember, IReadOnlyFolder) FindChildAndParentOrThrow(Guid childGuid);
+
     /// <summary>
     /// Finds the path to the member with <paramref name="guid"/>, the first element will be the member
     /// </summary>

+ 9 - 37
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -2,6 +2,7 @@
 using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using SkiaSharp;
+using ChunkyImageLib;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 
@@ -42,15 +43,15 @@ internal static class FloodFillHelper
             static (EmptyChunk _) => SKColors.Transparent
         );
 
-        if (colorToReplace.Alpha == 0 && drawingColor.Alpha == 0 || colorToReplace == drawingColor)
+        if ((colorToReplace.Alpha == 0 && drawingColor.Alpha == 0) || colorToReplace == drawingColor)
             return new();
 
         RectI globalSelectionBounds = (RectI?)selection?.TightBounds ?? new RectI(VecI.Zero, document.Size);
 
         // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.
         // Used for faster pixel checking
-        FloodFillColorRange colorRange = new(colorToReplace);
-        ulong uLongColor = ToULong(drawingColor);
+        ColorBounds colorRange = new(colorToReplace);
+        ulong uLongColor = drawingColor.ToULong();
 
         Dictionary<VecI, Chunk> drawingChunks = new();
         HashSet<VecI> processedEmptyChunks = new();
@@ -124,35 +125,6 @@ internal static class FloodFillHelper
         return drawingChunks;
     }
 
-    private static unsafe ulong ToULong(SKColor color)
-    {
-        ulong result = 0;
-        Half* ptr = (Half*)&result;
-        float normalizedAlpha = color.Alpha / 255.0f;
-        ptr[0] = (Half)(color.Red / 255f * normalizedAlpha);
-        ptr[1] = (Half)(color.Green / 255f * normalizedAlpha);
-        ptr[2] = (Half)(color.Blue / 255f * normalizedAlpha);
-        ptr[3] = (Half)(normalizedAlpha);
-        return result;
-    }
-
-    private static unsafe bool IsWithinBounds(ref FloodFillColorRange bounds, Half* pixel)
-    {
-        float r = (float)pixel[0];
-        float g = (float)pixel[1];
-        float b = (float)pixel[2];
-        float a = (float)pixel[3];
-        if (r < bounds.LowerR || r > bounds.UpperR)
-            return false;
-        if (g < bounds.LowerG || g > bounds.UpperG)
-            return false;
-        if (b < bounds.LowerB || b > bounds.UpperB)
-            return false;
-        if (a < bounds.LowerA || a > bounds.UpperA)
-            return false;
-        return true;
-    }
-
     private static unsafe byte[]? FloodFillChunk(
         Chunk referenceChunk,
         Chunk drawingChunk,
@@ -163,7 +135,7 @@ internal static class FloodFillHelper
         ulong colorBits,
         SKColor color,
         VecI pos,
-        FloodFillColorRange bounds)
+        ColorBounds bounds)
     {
         if (referenceChunk.Surface.GetSRGBPixel(pos) == color || drawingChunk.Surface.GetSRGBPixel(pos) == color)
             return null;
@@ -189,13 +161,13 @@ internal static class FloodFillHelper
             *(ulong*)drawPixel = colorBits;
             pixelStates[pixelOffset] = Visited;
 
-            if (curPos.X > 0 && pixelStates[pixelOffset - 1] == InSelection && IsWithinBounds(ref bounds, refPixel - 4))
+            if (curPos.X > 0 && pixelStates[pixelOffset - 1] == InSelection && bounds.IsWithinBounds(refPixel - 4))
                 toVisit.Push(new(curPos.X - 1, curPos.Y));
-            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection && IsWithinBounds(ref bounds, refPixel + 4))
+            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection && bounds.IsWithinBounds(refPixel + 4))
                 toVisit.Push(new(curPos.X + 1, curPos.Y));
-            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection && IsWithinBounds(ref bounds, refPixel - 4 * chunkSize))
+            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection && bounds.IsWithinBounds(refPixel - 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y - 1));
-            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection && IsWithinBounds(ref bounds, refPixel + 4 * chunkSize))
+            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection && bounds.IsWithinBounds(refPixel + 4 * chunkSize))
                 toVisit.Push(new(curPos.X, curPos.Y + 1));
         }
         return pixelStates;

+ 68 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/ReplaceColor_Change.cs

@@ -0,0 +1,68 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+internal class ReplaceColor_Change : Change
+{
+    private readonly SKColor oldColor;
+    private readonly SKColor newColor;
+
+    private Dictionary<Guid, CommittedChunkStorage>? savedChunks;
+
+    [GenerateMakeChangeAction]
+    public ReplaceColor_Change(SKColor oldColor, SKColor newColor)
+    {
+        this.oldColor = oldColor;
+        this.newColor = newColor;
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        return new Success();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        if (savedChunks is not null)
+            throw new InvalidOperationException();
+        savedChunks = new();
+        List<IChangeInfo> infos = new();
+        target.ForEveryMember(member =>
+        {
+            if (member is not Layer layer)
+                return;
+            layer.LayerImage.EnqueueReplaceColor(oldColor, newColor);
+            HashSet<VecI>? chunks = layer.LayerImage.FindAffectedChunks();
+            CommittedChunkStorage storage = new(layer.LayerImage, chunks);
+            savedChunks[layer.GuidValue] = storage;
+            layer.LayerImage.CommitChanges();
+            infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks));
+        });
+        ignoreInUndo = !savedChunks.Any();
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        if (savedChunks is null)
+            throw new InvalidOperationException();
+        List<IChangeInfo> infos = new();
+        target.ForEveryMember(member =>
+        {
+            if (member is not Layer layer)
+                return;
+            CommittedChunkStorage? storage = savedChunks[member.GuidValue];
+            var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(layer.LayerImage, ref storage);
+            infos.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, chunks));
+        });
+        savedChunks = null;
+        return infos;
+    }
+
+    public override void Dispose()
+    {
+        if (savedChunks is null)
+            return;
+        foreach (var storage in savedChunks.Values)
+            storage.Dispose();
+    }
+}

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EllipseToolExecutor.cs

@@ -40,6 +40,7 @@ internal class EllipseToolExecutor : UpdateableChangeExecutor
         strokeWidth = toolbar.ToolSize;
         memberGuid = member.GuidValue;
 
+        colorsVM.AddSwatch(strokeColor);
         DrawEllipseOrCircle(startPos);
         return new Success();
     }

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -33,6 +33,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         toolSize = toolbar.ToolSize;
         pixelPerfect = toolbar.PixelPerfectEnabled;
 
+        vm.ColorsSubViewModel.AddSwatch(color);
         IAction? action = pixelPerfect switch
         {
             false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, toolSize, false, drawOnMask),

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

@@ -10,6 +10,7 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentPassthroughActions;
@@ -124,7 +125,9 @@ internal class DocumentViewModel : NotifyableObject
 
     private SKPath selectionPath = new SKPath();
     public SKPath SelectionPathBindable => selectionPath;
-    
+
+    public WpfObservableRangeCollection<SKColor> Swatches { get; set; } = new WpfObservableRangeCollection<SKColor>();
+    public WpfObservableRangeCollection<SKColor> Palette { get; set; } = new WpfObservableRangeCollection<SKColor>();
 
     public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();
     public ExecutionTrigger<double> ZoomViewportTrigger { get; } = new ExecutionTrigger<double>();
@@ -272,6 +275,13 @@ internal class DocumentViewModel : NotifyableObject
         Helpers.ActionAccumulator.AddFinishedActions(new ResizeImage_Action(newSize, resampling));
     }
 
+    public void ReplaceColor(SKColor oldColor, SKColor newColor)
+    {
+        if (Helpers.ChangeController.IsChangeActive || oldColor == newColor)
+            return;
+        Helpers.ActionAccumulator.AddFinishedActions(new ReplaceColor_Action(oldColor, newColor));
+    }
+
     public void SetSelectedMember(Guid memberGuid) => Helpers.ActionAccumulator.AddActions(new SetSelectedMember_PassthroughAction(memberGuid));
 
     public void AddSoftSelectedMember(Guid memberGuid) => Helpers.ActionAccumulator.AddActions(new AddSoftSelectedMember_PassthroughAction(memberGuid));

+ 27 - 45
src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -42,7 +42,6 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
             if (primaryColor != value)
             {
                 primaryColor = value;
-                //Owner.BitmapManager.PrimaryColor = value;
                 RaisePropertyChanged(nameof(PrimaryColor));
             }
         }
@@ -73,31 +72,16 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Colors.CanReplaceColors")]
     public bool CanReplaceColors()
     {
-        //return ViewModelMain.Current?.BitmapManager?.ActiveDocument is not null;
-        return false;
+        return ViewModelMain.Current?.DocumentManagerSubViewModel?.ActiveDocument is not null;
     }
 
     [Command.Internal("PixiEditor.Colors.ReplaceColors")]
     public void ReplaceColors((SKColor oldColor, SKColor newColor) colors)
     {
-
-    }
-
-    private static void ReplacePaletteColorProcess(object[] args)
-    {
-        //(SKColor oldColor, SKColor newColor) colors = ((SKColor, SKColor))args[0];
-        //Document activeDocument = (Document)args[1];
-
-        //ReplacePaletteColor(colors, activeDocument);
-    }
-
-    private static void ReplacePaletteColor((SKColor oldColor, SKColor newColor) colors, DocumentViewModel activeDocument)
-    {
-        /*int oldIndex = activeDocument.Palette.IndexOf(colors.oldColor);
-        if (oldIndex != -1)
-        {
-            activeDocument.Palette[oldIndex] = colors.newColor;
-        }*/
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null || colors.oldColor == colors.newColor)
+            return;
+        doc.ReplaceColor(colors.oldColor, colors.newColor);
     }
 
     private async void OwnerOnStartupEvent(object sender, EventArgs e)
@@ -106,7 +90,12 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Colors.OpenPaletteBrowser", "Open Palette Browser", "Open Palette Browser", CanExecute = "PixiEditor.HasDocument", IconPath = "Globe.png")]
-    public void OpenPalettesBrowser() { } // PalettesBrowser.Open(PaletteDataSources, ImportPaletteCommand, Owner.BitmapManager.ActiveDocument.Palette);
+    public void OpenPalettesBrowser() 
+    {
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is not null)
+            PalettesBrowser.Open(PaletteDataSources, ImportPaletteCommand, doc.Palette);
+    } 
 
     private async Task ImportLospecPalette()
     {
@@ -176,8 +165,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     [Command.Internal("PixiEditor.Colors.ImportPalette", CanExecute = "PixiEditor.Colors.CanImportPalette")]
     public void ImportPalette(List<string> palette)
     {
-        /*
-        var doc = Owner.BitmapManager.ActiveDocument;
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
 
@@ -190,17 +178,13 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
 
             doc.Palette.ReplaceRange(palette.Select(x => SKColor.Parse(x)));
         }
-        */
     }
 
     [Evaluator.CanExecute("PixiEditor.Colors.CanSelectPaletteColor")]
     public bool CanSelectPaletteColor(int index)
     {
-        /*
-        var document = Owner.BitmapManager.ActiveDocument;
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
         return document?.Palette is not null && document.Palette.Count > index;
-        */
-        return false;
     }
 
     [Evaluator.Icon("PixiEditor.Colors.FirstPaletteColorIcon")]
@@ -227,8 +211,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
 
     private ImageSource GetPaletteColorIcon(int index)
     {
-        /*
-        var document = Owner.BitmapManager.ActiveDocument;
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
         SKColor color;
         if (document?.Palette is null || document.Palette.Count <= index)
@@ -236,8 +219,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         else
             color = document.Palette[index];
 
-        return ColorSearchResult.GetIcon(color);*/
-        return ColorSearchResult.GetIcon(SKColors.Transparent);
+        return ColorSearchResult.GetIcon(color);
     }
 
     [Command.Basic("PixiEditor.Colors.SelectFirstPaletteColor", "Select color 1", "Select the first color in the palette", Key = Key.D1, Parameter = 0, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.FirstPaletteColorIcon")]
@@ -252,13 +234,11 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Colors.SelectTenthPaletteColor", "Select color 10", "Select the tenth color in the palette", Key = Key.D0, Parameter = 9, CanExecute = "PixiEditor.Colors.CanSelectPaletteColor", IconEvaluator = "PixiEditor.Colors.TenthPaletteColorIcon")]
     public void SelectPaletteColor(int index)
     {
-        /*
-        var document = Owner.BitmapManager.ActiveDocument;
-        if (document.Palette != null && document.Palette.Count > index)
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (document?.Palette is not null && document.Palette.Count > index)
         {
             PrimaryColor = document.Palette[index];
         }
-        */
     }
 
     [Command.Basic("PixiEditor.Colors.Swap", "Swap colors", "Swap primary and secondary colors", Key = Key.X)]
@@ -269,23 +249,25 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
 
     public void AddSwatch(SKColor color)
     {
-        /*
-        if (!Owner.BitmapManager.ActiveDocument.Swatches.Contains(color))
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+        if (!doc.Swatches.Contains(color))
         {
-            Owner.BitmapManager.ActiveDocument.Swatches.Add(color);
+            doc.Swatches.Add(color);
         }
-        */
     }
 
     [Command.Internal("PixiEditor.Colors.RemoveSwatch")]
     public void RemoveSwatch(SKColor color)
     {
-        /*
-        if (Owner.BitmapManager.ActiveDocument.Swatches.Contains(color))
+        var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (doc is null)
+            return;
+        if (doc.Swatches.Contains(color))
         {
-            Owner.BitmapManager.ActiveDocument.Swatches.Remove(color);
+            doc.Swatches.Remove(color);
         }
-        */
     }
 
     [Command.Internal("PixiEditor.Colors.SelectColor")]

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -107,7 +107,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     [Evaluator.CanExecute("PixiEditor.Layer.CanCreateNewMember")]
     public bool CanCreateNewMember(object parameter)
     {
-        return Owner.DocumentManagerSubViewModel.ActiveDocument is not null;
+        return Owner.DocumentManagerSubViewModel.ActiveDocument is { } doc && !doc.UpdateableChangeActive;
     }
 
     [Command.Internal("PixiEditor.Layer.OpacitySliderDragStarted")]

+ 6 - 4
src/PixiEditor/Views/MainWindow.xaml

@@ -626,10 +626,12 @@
                                                 <palettes:CompactPaletteViewer 
                                                     IsEnabled="{Binding DocumentSubViewModel.Owner.BitmapManager.ActiveDocument, Converter={converters:NotNullToBoolConverter}}"
                                                     SelectColorCommand="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
-                                                    Colors="{Binding BitmapManager.ActiveDocument.Palette}" 
+                                                    Colors="{Binding DocumentManagerSubViewModel.ActiveDocument.Palette}" 
                                                     Visibility="{Binding RelativeSource={RelativeSource Mode=Self}, Path=ActualWidth, Converter={converters:PaletteViewerWidthToVisibilityConverter}}"/>
-                                                <palettes:PaletteViewer IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Converter={converters:NotNullToBoolConverter}}" Colors="{Binding BitmapManager.ActiveDocument.Palette}"
-                                                    Swatches="{Binding BitmapManager.ActiveDocument.Swatches}"
+                                                <palettes:PaletteViewer 
+                                                    IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Converter={converters:NotNullToBoolConverter}}" 
+                                                    Colors="{Binding DocumentManagerSubViewModel.ActiveDocument.Palette}"
+                                                    Swatches="{Binding DocumentManagerSubViewModel.ActiveDocument.Swatches}"
                                                     SelectColorCommand="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
                                                     HintColor="{Binding Path=ColorsSubViewModel.PrimaryColor,Converter={converters:SKColorToMediaColorConverter}}"
                                                     DataSources="{Binding ColorsSubViewModel.PaletteDataSources}"
@@ -649,7 +651,7 @@
 										CanFloat="True">
                                             <usercontrols:SwatchesView
 											    SelectSwatchCommand="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
-											    Swatches="{Binding BitmapManager.ActiveDocument.Swatches}" />
+											    Swatches="{Binding DocumentManagerSubViewModel.ActiveDocument.Swatches}" />
                                         </avalondock:LayoutAnchorable>
                                     </LayoutAnchorablePane>
                                     <LayoutAnchorablePane>