Browse Source

Revert linear color space, floodfill slow impl

Equbuxu 3 years ago
parent
commit
1c71f1ce93

+ 8 - 2
src/ChunkyImageLib/ChunkyImage.cs

@@ -300,11 +300,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
-    public void EnqueueDrawImage(VecI pos, Surface image)
+    /// <summary>
+    /// Be careful about the copyImage argument. The default is true, and this is a thread safe version without any side effects. 
+    /// It will hovewer copy the surface right away which can be slow (in updateable changes especially). 
+    /// If copyImage is set to false, the image won't be copied and instead a reference will be stored.
+    /// Surface is NOT THREAD SAFE, so if you pass a Surface here with copyImage == false you must not do anything with that surface anywhere (not even read) until CommitChanges/CancelChanges is called.
+    /// </summary>
+    public void EnqueueDrawImage(VecI pos, Surface image, bool copyImage = true)
     {
         lock (lockObject)
         {
-            ImageOperation operation = new(pos, image);
+            ImageOperation operation = new(pos, image, copyImage);
             EnqueueOperation(operation);
         }
     }

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

@@ -27,6 +27,14 @@ public struct VecD
     {
         return new(Math.Round(X), Math.Round(Y));
     }
+    public VecD Ceiling()
+    {
+        return new(Math.Ceiling(X), Math.Ceiling(Y));
+    }
+    public VecD Floor()
+    {
+        return new(Math.Floor(X), Math.Floor(Y));
+    }
     public VecD Rotate(double angle)
     {
         VecD result = new VecD();

+ 18 - 7
src/ChunkyImageLib/Surface.cs

@@ -13,6 +13,8 @@ public class Surface : IDisposable
     public SKSurface SkiaSurface { get; }
     public VecI Size { get; }
 
+    private SKPaint drawingPaint = new SKPaint() { BlendMode = SKBlendMode.Src };
+
     public Surface(VecI size)
     {
         if (size.X < 1 || size.Y < 1)
@@ -49,16 +51,24 @@ public class Surface : IDisposable
         if (other.Size != Size)
             throw new ArgumentException("Target Surface must have the same dimensions");
         int bytesC = Size.X * Size.Y * bytesPerPixel;
-        Buffer.MemoryCopy((void*)PixelBuffer, (void*)other.PixelBuffer, bytesC, bytesC);
+        using var pixmap = other.SkiaSurface.PeekPixels();
+        Buffer.MemoryCopy((void*)PixelBuffer, (void*)pixmap.GetPixels(), bytesC, bytesC);
     }
 
-    public unsafe SKColor GetSRGBPixel(int x, int y)
+    /// <summary>
+    /// Consider getting a pixmap from SkiaSurface.PeekPixels().GetPixels() and writing into it's buffer for bulk pixel get/set. Don't forget to dispose the pixmap afterwards.
+    /// </summary>
+    public unsafe SKColor GetSRGBPixel(VecI pos)
     {
-        throw new NotImplementedException("This function needs to be changed to account for linear -> srgb conversion");
-        /*
-        Half* ptr = (Half*)(PixelBuffer + (x + y * Size.X) * bytesPerPixel);
+        Half* ptr = (Half*)(PixelBuffer + (pos.X + pos.Y * Size.X) * bytesPerPixel);
         float a = (float)ptr[3];
-        return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);*/
+        return (SKColor)new SKColorF((float)ptr[0] / a, (float)ptr[1] / a, (float)ptr[2] / a, (float)ptr[3]);
+    }
+
+    public void SetSRGBPixel(VecI pos, SKColor color)
+    {
+        drawingPaint.Color = color;
+        SkiaSurface.Canvas.DrawPoint(pos.X, pos.Y, drawingPaint);
     }
 
     public unsafe bool IsFullyTransparent()
@@ -88,7 +98,7 @@ public class Surface : IDisposable
 
     private SKSurface CreateSKSurface()
     {
-        var surface = SKSurface.Create(new SKImageInfo(Size.X, Size.Y, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgbLinear()), PixelBuffer);
+        var surface = SKSurface.Create(new SKImageInfo(Size.X, Size.Y, SKColorType.RgbaF16, SKAlphaType.Premul, SKColorSpace.CreateSrgb()), PixelBuffer);
         if (surface is null)
             throw new InvalidOperationException($"Could not create surface (Size:{Size})");
         return surface;
@@ -107,6 +117,7 @@ public class Surface : IDisposable
         if (disposed)
             return;
         disposed = true;
+        drawingPaint.Dispose();
         Marshal.FreeHGlobal(PixelBuffer);
         GC.SuppressFinalize(this);
     }

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

@@ -0,0 +1,40 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+internal class FloodFillChunkStorage : IDisposable
+{
+    private readonly ChunkyImage image;
+    private Dictionary<VecI, Chunk> acquiredChunks = new();
+    private SKPaint ReplacingPaint { get; } = new SKPaint() { BlendMode = SKBlendMode.Src };
+
+    public FloodFillChunkStorage(ChunkyImage image)
+    {
+        this.image = image;
+    }
+
+    public Chunk GetChunk(VecI pos)
+    {
+        if (acquiredChunks.ContainsKey(pos))
+            return acquiredChunks[pos];
+        Chunk chunk = Chunk.Create(ChunkResolution.Full);
+        if (!image.DrawMostUpToDateChunkOn(pos, ChunkResolution.Full, chunk.Surface.SkiaSurface, new(0, 0), ReplacingPaint))
+            chunk.Surface.SkiaSurface.Canvas.Clear();
+        acquiredChunks[pos] = chunk;
+        return chunk;
+    }
+
+    public void DrawOnImage()
+    {
+        foreach (var (pos, chunk) in acquiredChunks)
+        {
+            image.EnqueueDrawImage(pos * ChunkResolution.Full.PixelSize(), chunk.Surface, false);
+        }
+    }
+
+    public void Dispose()
+    {
+        foreach (var chunk in acquiredChunks.Values)
+            chunk.Dispose();
+        ReplacingPaint.Dispose();
+    }
+}

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

@@ -0,0 +1,38 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+internal struct FloodFillColorBounds
+{
+    public float LowerR { get; set; }
+    public float LowerG { get; set; }
+    public float LowerB { get; set; }
+    public float LowerA { get; set; }
+    public float UpperR { get; set; }
+    public float UpperG { get; set; }
+    public float UpperB { get; set; }
+    public float UpperA { get; set; }
+
+    public FloodFillColorBounds(SKColor color)
+    {
+        static (float lower, float upper) FindInclusiveBoundaryPremul(byte channel, float alpha)
+        {
+            float subHalf = channel > 0 ? channel - .5f : channel;
+            float addHalf = channel < 255 ? channel + .5f : channel;
+            return (subHalf * alpha / 255f, addHalf * alpha / 255f);
+        }
+
+        static (float lower, float upper) FindInclusiveBoundary(byte channel)
+        {
+            float subHalf = channel > 0 ? channel - .5f : channel;
+            float addHalf = channel < 255 ? channel + .5f : channel;
+            return (subHalf / 255f, addHalf / 255f);
+        }
+
+        float a = color.Alpha / 255f;
+
+        (LowerR, UpperR) = FindInclusiveBoundaryPremul(color.Red, a);
+        (LowerG, UpperG) = FindInclusiveBoundaryPremul(color.Green, a);
+        (LowerB, UpperB) = FindInclusiveBoundaryPremul(color.Blue, a);
+        (LowerA, UpperA) = FindInclusiveBoundary(color.Alpha);
+    }
+}

+ 109 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -0,0 +1,109 @@
+using ChunkyImageLib.Operations;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+internal class FloodFillHelper
+{
+    public static (HashSet<VecI>, CommittedChunkStorage) FloodFillAndCommit(ChunkyImage image, VecI pos, SKColor color)
+    {
+        int chunkSize = ChunkResolution.Full.PixelSize();
+
+        using FloodFillChunkStorage storage = new(image);
+
+        VecI initChunkPos = OperationHelper.GetChunkPos(pos, chunkSize);
+        VecI imageSizeInChunks = (VecI)(image.LatestSize / (double)chunkSize).Ceiling();
+        VecI initPosOnChunk = pos - initChunkPos * chunkSize;
+        SKColor colorToReplace = storage.GetChunk(initChunkPos).Surface.GetSRGBPixel(initPosOnChunk);
+
+        FloodFillColorBounds bounds = new(colorToReplace);
+        ulong uLongColor = ToULong(color);
+
+        Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
+        positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
+        while (positionsToFloodFill.Count > 0)
+        {
+            var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
+            Chunk chunk = storage.GetChunk(chunkPos);
+            var maybeArray = FloodFillChunk(chunk, chunkSize, uLongColor, color, posOnChunk, bounds);
+            if (maybeArray is null)
+                continue;
+            for (int i = 0; i < chunkSize; i++)
+            {
+                if (chunkPos.Y > 0 && maybeArray[i])
+                    positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
+                if (chunkPos.Y < imageSizeInChunks.Y - 1 && maybeArray[chunkSize * (chunkSize - 1) + i])
+                    positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
+                if (chunkPos.X > 0 && maybeArray[i * chunkSize])
+                    positionsToFloodFill.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
+                if (chunkPos.X < imageSizeInChunks.X - 1 && maybeArray[i * chunkSize + (chunkSize - 1)])
+                    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);
+    }
+
+    private unsafe static 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 unsafe static bool IsWithinBounds(ref FloodFillColorBounds 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 unsafe static bool[]? FloodFillChunk(Chunk chunk, int chunkSize, ulong colorBits, SKColor color, VecI pos, FloodFillColorBounds bounds)
+    {
+        if (chunk.Surface.GetSRGBPixel(pos) == color)
+            return null;
+
+        bool[] visited = new bool[chunkSize * chunkSize];
+        using var pixmap = chunk.Surface.SkiaSurface.PeekPixels();
+        Half* array = (Half*)pixmap.GetPixels();
+
+        Stack<VecI> toVisit = new();
+        toVisit.Push(pos);
+
+        while (toVisit.Count > 0)
+        {
+            VecI curPos = toVisit.Pop();
+            int pixelOffset = curPos.X + curPos.Y * chunkSize;
+            Half* pixel = array + pixelOffset * 4;
+            *(ulong*)pixel = colorBits;
+            visited[pixelOffset] = true;
+
+            if (curPos.X > 0 && !visited[pixelOffset - 1] && IsWithinBounds(ref bounds, pixel - 4))
+                toVisit.Push(new(curPos.X - 1, curPos.Y));
+            if (curPos.X < chunkSize - 1 && !visited[pixelOffset + 1] && IsWithinBounds(ref bounds, pixel + 4))
+                toVisit.Push(new(curPos.X + 1, curPos.Y));
+            if (curPos.Y > 0 && !visited[pixelOffset - chunkSize] && IsWithinBounds(ref bounds, pixel - 4 * chunkSize))
+                toVisit.Push(new(curPos.X, curPos.Y - 1));
+            if (curPos.Y < chunkSize - 1 && !visited[pixelOffset + chunkSize] && IsWithinBounds(ref bounds, pixel + 4 * chunkSize))
+                toVisit.Push(new(curPos.X, curPos.Y + 1));
+        }
+        return visited;
+    }
+}

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

@@ -0,0 +1,55 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+internal class FloodFill_Change : Change
+{
+    private readonly Guid memberGuid;
+    private readonly VecI pos;
+    private readonly SKColor color;
+    private readonly bool drawOnMask;
+    private CommittedChunkStorage? chunkStorage = null;
+
+    [GenerateMakeChangeAction]
+    public FloodFill_Change(Guid memberGuid, VecI pos, SKColor color, bool drawOnMask)
+    {
+        this.memberGuid = memberGuid;
+        this.pos = pos;
+        this.color = color;
+        this.drawOnMask = drawOnMask;
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (pos.X < 0 || pos.Y < 0 || pos.X >= target.Size.X || pos.Y >= target.Size.X)
+            return new Error();
+        if (!DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask))
+            return new Error();
+        return new Success();
+    }
+
+    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    {
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
+        (var affectedChunks, chunkStorage) = FloodFillHelper.FloodFillAndCommit(image, pos, color);
+        ignoreInUndo = false;
+        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+    }
+
+    public override IChangeInfo? Revert(Document target)
+    {
+        if (chunkStorage is null)
+            throw new InvalidOperationException("No saved chunks to revert to");
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
+        chunkStorage.ApplyChunksToImage(image);
+        var affectedChunks = image.FindAffectedChunks();
+        image.CommitChanges();
+        chunkStorage.Dispose();
+        chunkStorage = null;
+        return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, affectedChunks, drawOnMask);
+    }
+
+    public override void Dispose()
+    {
+        chunkStorage?.Dispose();
+    }
+}

+ 1 - 0
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -5,6 +5,7 @@
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>

+ 2 - 1
src/PixiEditorPrototype/Models/Tool.cs

@@ -3,5 +3,6 @@
 internal enum Tool
 {
     Rectangle,
-    Select
+    Select,
+    FloodFill
 }

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

@@ -251,6 +251,13 @@ internal class DocumentViewModel : INotifyPropertyChanged
         }
     }
 
+    public void FloodFill(VecI pos, SKColor color)
+    {
+        if (updateableChangeActive || SelectedStructureMember is null)
+            return;
+        Helpers.ActionAccumulator.AddFinishedActions(new FloodFill_Action(SelectedStructureMember.GuidValue, pos, color, SelectedStructureMember.ShouldDrawOnMask));
+    }
+
     public void AddOrUpdateViewport(ViewportLocation location)
     {
         Helpers.ActionAccumulator.AddActions(new RefreshViewport_PassthroughAction(location));

+ 4 - 0
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -97,6 +97,10 @@ internal class ViewModelMain : INotifyPropertyChanged
         var pos = args.GetPosition(source);
         mouseDownCanvasX = (int)(pos.X / source.Width * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelWidth);
         mouseDownCanvasY = (int)(pos.Y / source.Height * ActiveDocument.Bitmaps[ChunkResolution.Full].PixelHeight);
+        if (activeTool is Tool.FloodFill)
+        {
+            ActiveDocument.FloodFill(new VecI(mouseDownCanvasX, mouseDownCanvasY), new SKColor(SelectedColor.R, SelectedColor.G, SelectedColor.B, SelectedColor.A));
+        }
     }
 
     private void MouseMove(object? param)

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

@@ -184,6 +184,7 @@
             <StackPanel Orientation="Vertical" Background="White">
                 <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Rectangle}">Rect</Button>
                 <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.Select}">Select</Button>
+                <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.FloodFill}">Fill</Button>
                 <colorpicker:PortableColorPicker Margin="5" SelectedColor="{Binding SelectedColor, Mode=TwoWay}" Width="30" Height="30"/>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding NormalZoombox, Mode=OneWayToSource}">Normal</RadioButton>
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding MoveZoombox, Mode=OneWayToSource}">Move</RadioButton>