Browse Source

Improve floodfill slightly

Equbuxu 3 years ago
parent
commit
cd94e3c32a

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

@@ -12,6 +12,11 @@ internal class FloodFillChunkStorage : IDisposable
         this.image = image;
         this.image = image;
     }
     }
 
 
+    public bool ChunkExistsInStorageOrInImage(VecI pos)
+    {
+        return acquiredChunks.ContainsKey(pos) || image.LatestOrCommittedChunkExists(pos);
+    }
+
     public Chunk GetChunk(VecI pos)
     public Chunk GetChunk(VecI pos)
     {
     {
         if (acquiredChunks.ContainsKey(pos))
         if (acquiredChunks.ContainsKey(pos))
@@ -23,12 +28,16 @@ internal class FloodFillChunkStorage : IDisposable
         return chunk;
         return chunk;
     }
     }
 
 
-    public void DrawOnImage()
+    public (CommittedChunkStorage, HashSet<VecI>) DrawOnChunkyImage(ChunkyImage chunkyImage)
     {
     {
         foreach (var (pos, chunk) in acquiredChunks)
         foreach (var (pos, chunk) in acquiredChunks)
         {
         {
-            image.EnqueueDrawImage(pos * ChunkResolution.Full.PixelSize(), chunk.Surface, false);
+            chunkyImage.EnqueueDrawImage(pos * ChunkResolution.Full.PixelSize(), chunk.Surface, false);
         }
         }
+        var affected = chunkyImage.FindAffectedChunks();
+        var affectedChunkStorage = new CommittedChunkStorage(chunkyImage, affected);
+        chunkyImage.CommitChanges();
+        return (affectedChunkStorage, affected);
     }
     }
 
 
     public void Dispose()
     public void Dispose()

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

@@ -1,7 +1,7 @@
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
-internal struct FloodFillColorBounds
+internal struct FloodFillColorRange
 {
 {
     public float LowerR { get; set; }
     public float LowerR { get; set; }
     public float LowerG { get; set; }
     public float LowerG { get; set; }
@@ -12,7 +12,7 @@ internal struct FloodFillColorBounds
     public float UpperB { get; set; }
     public float UpperB { get; set; }
     public float UpperA { get; set; }
     public float UpperA { get; set; }
 
 
-    public FloodFillColorBounds(SKColor color)
+    public FloodFillColorRange(SKColor color)
     {
     {
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
         {
         {

+ 41 - 14
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -4,27 +4,58 @@ using SkiaSharp;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 internal class FloodFillHelper
 internal class FloodFillHelper
 {
 {
-    public static (HashSet<VecI>, CommittedChunkStorage) FloodFillAndCommit(ChunkyImage image, VecI pos, SKColor color)
+    public static FloodFillChunkStorage FloodFill(ChunkyImage image, VecI startingPos, SKColor drawingColor)
     {
     {
         int chunkSize = ChunkResolution.Full.PixelSize();
         int chunkSize = ChunkResolution.Full.PixelSize();
 
 
-        using FloodFillChunkStorage storage = new(image);
+        FloodFillChunkStorage storage = new(image);
 
 
-        VecI initChunkPos = OperationHelper.GetChunkPos(pos, chunkSize);
+        VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
         VecI imageSizeInChunks = (VecI)(image.LatestSize / (double)chunkSize).Ceiling();
         VecI imageSizeInChunks = (VecI)(image.LatestSize / (double)chunkSize).Ceiling();
-        VecI initPosOnChunk = pos - initChunkPos * chunkSize;
+        VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
         SKColor colorToReplace = storage.GetChunk(initChunkPos).Surface.GetSRGBPixel(initPosOnChunk);
         SKColor colorToReplace = storage.GetChunk(initChunkPos).Surface.GetSRGBPixel(initPosOnChunk);
 
 
-        FloodFillColorBounds bounds = new(colorToReplace);
-        ulong uLongColor = ToULong(color);
+        if (colorToReplace.Alpha == 0 && drawingColor.Alpha == 0 || colorToReplace == drawingColor)
+            return storage;
 
 
+        // Premultiplies the color and convert it to floats. Since floats are inprecise, a range is used.
+        // Used for faster pixel checking
+        FloodFillColorRange bounds = new(colorToReplace);
+        ulong uLongColor = ToULong(drawingColor);
+
+        // flood fill chunks using a basic 4-way approach with a stack (each chunk is kinda like a pixel)
+        // once the chunk is filled all places where it spills over to neighboring chunks are saved in the stack
         Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
         Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
         positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
         positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
         while (positionsToFloodFill.Count > 0)
         while (positionsToFloodFill.Count > 0)
         {
         {
             var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
             var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
+
+            // if the chunks is empty and we are replacing a transparent color clear the whole chunk right away
+            if (!storage.ChunkExistsInStorageOrInImage(chunkPos))
+            {
+                if (colorToReplace.Alpha == 0)
+                {
+                    var chunkToClear = storage.GetChunk(chunkPos);
+                    chunkToClear.Surface.SkiaSurface.Canvas.Clear(drawingColor);
+                    for (int i = 0; i < chunkSize; i++)
+                    {
+                        if (chunkPos.Y > 0)
+                            positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
+                        if (chunkPos.Y < imageSizeInChunks.Y - 1)
+                            positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
+                        if (chunkPos.X > 0)
+                            positionsToFloodFill.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
+                        if (chunkPos.X < imageSizeInChunks.X - 1)
+                            positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
+                    }
+                }
+                continue;
+            }
+
+            // use regular flood fill for chunks that have something in them
             Chunk chunk = storage.GetChunk(chunkPos);
             Chunk chunk = storage.GetChunk(chunkPos);
-            var maybeArray = FloodFillChunk(chunk, chunkSize, uLongColor, color, posOnChunk, bounds);
+            var maybeArray = FloodFillChunk(chunk, chunkSize, uLongColor, drawingColor, posOnChunk, bounds);
             if (maybeArray is null)
             if (maybeArray is null)
                 continue;
                 continue;
             for (int i = 0; i < chunkSize; i++)
             for (int i = 0; i < chunkSize; i++)
@@ -39,11 +70,7 @@ internal class FloodFillHelper
                     positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
                     positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
             }
             }
         }
         }
-        storage.DrawOnImage();
-        var affected = image.FindAffectedChunks();
-        var affectedChunkStorage = new CommittedChunkStorage(image, affected);
-        image.CommitChanges();
-        return (affected, affectedChunkStorage);
+        return storage;
     }
     }
 
 
     private unsafe static ulong ToULong(SKColor color)
     private unsafe static ulong ToULong(SKColor color)
@@ -58,7 +85,7 @@ internal class FloodFillHelper
         return result;
         return result;
     }
     }
 
 
-    private unsafe static bool IsWithinBounds(ref FloodFillColorBounds bounds, Half* pixel)
+    private unsafe static bool IsWithinBounds(ref FloodFillColorRange bounds, Half* pixel)
     {
     {
         float r = (float)pixel[0];
         float r = (float)pixel[0];
         float g = (float)pixel[1];
         float g = (float)pixel[1];
@@ -75,7 +102,7 @@ internal class FloodFillHelper
         return true;
         return true;
     }
     }
 
 
-    private unsafe static bool[]? FloodFillChunk(Chunk chunk, int chunkSize, ulong colorBits, SKColor color, VecI pos, FloodFillColorBounds bounds)
+    private unsafe static bool[]? FloodFillChunk(Chunk chunk, int chunkSize, ulong colorBits, SKColor color, VecI pos, FloodFillColorRange bounds)
     {
     {
         if (chunk.Surface.GetSRGBPixel(pos) == color)
         if (chunk.Surface.GetSRGBPixel(pos) == color)
             return null;
             return null;

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

@@ -30,7 +30,10 @@ internal class FloodFill_Change : Change
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-        (var affectedChunks, chunkStorage) = FloodFillHelper.FloodFillAndCommit(image, pos, color);
+
+        using var floodFilledChunks = FloodFillHelper.FloodFill(image, pos, color);
+        (chunkStorage, var affectedChunks) = floodFilledChunks.DrawOnChunkyImage(image);
+
         ignoreInUndo = false;
         ignoreInUndo = false;
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
     }
     }

+ 1 - 0
src/README.md

@@ -109,6 +109,7 @@ Decouples the state of a document from the UI.
         - [x] Create/Delete mask
         - [x] Create/Delete mask
         - [ ] Apply mask
         - [ ] Apply mask
 - ViewModel
 - ViewModel
+    - [ ] Loading window when background thread is busy
     - [ ] Action filtering
     - [ ] Action filtering
     - [x] Transform overlay
     - [x] Transform overlay
     - [x] Symmetry overlay
     - [x] Symmetry overlay