123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- using ChunkyImageLib.Operations;
- using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
- using PixiEditor.ChangeableDocument.Changeables.Interfaces;
- using Drawie.Backend.Core;
- using Drawie.Backend.Core.ColorsImpl;
- using Drawie.Backend.Core.Numerics;
- using Drawie.Backend.Core.Surfaces;
- using Drawie.Backend.Core.Surfaces.ImageData;
- using Drawie.Backend.Core.Vector;
- using Drawie.Numerics;
- namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
- public static class FloodFillHelper
- {
- private const byte InSelection = 1;
- private const byte Visited = 2;
- private static readonly VecI Up = new VecI(0, -1);
- private static readonly VecI Down = new VecI(0, 1);
- private static readonly VecI Left = new VecI(-1, 0);
- private static readonly VecI Right = new VecI(1, 0);
- internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document,
- int frame)
- {
- if (membersToFloodFill.Count == 1)
- {
- Guid guid = membersToFloodFill.First();
- var member = document.FindMemberOrThrow(guid);
- if (member is IReadOnlyFolderNode)
- return new FloodFillChunkCache(membersToFloodFill, document, frame);
- if (member is not IReadOnlyImageNode rasterLayer)
- throw new InvalidOperationException("Member is not a raster layer");
- return new FloodFillChunkCache(rasterLayer.GetLayerImageAtFrame(frame));
- }
- return new FloodFillChunkCache(membersToFloodFill, document, frame);
- }
- public static Dictionary<VecI, Chunk> FloodFill(
- HashSet<Guid> membersToFloodFill,
- IReadOnlyDocument document,
- VectorPath? selection,
- VecI startingPos,
- Color drawingColor,
- float tolerance,
- int frame)
- {
- if (selection is not null && !selection.Contains(startingPos.X + 0.5f, startingPos.Y + 0.5f))
- return new();
- int chunkSize = ChunkResolution.Full.PixelSize();
- FloodFillChunkCache cache = CreateCache(membersToFloodFill, document, frame);
- VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
- VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
- VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
- var chunkAtPos = cache.GetChunk(initChunkPos);
- Color colorToReplace = chunkAtPos.Match(
- (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
- static (EmptyChunk _) => Colors.Transparent
- );
- ulong uLongColor = drawingColor.ToULong();
- Color colorSpaceCorrectedColor = drawingColor;
- if (!document.ProcessingColorSpace.IsSrgb)
- {
- var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
- var fixedColor = drawingColor.TransformColor(srgbTransform);
- uLongColor = fixedColor.ToULong();
- colorSpaceCorrectedColor = fixedColor;
- }
- if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
- 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
- ColorBounds colorRange = new(colorToReplace, tolerance);
- Dictionary<VecI, Chunk> drawingChunks = new();
- HashSet<VecI> processedEmptyChunks = new();
- // 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();
- positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
- int iter = -1;
- while (positionsToFloodFill.Count > 0)
- {
- iter++;
- var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
- if (!drawingChunks.ContainsKey(chunkPos))
- {
- var chunk = Chunk.Create(document.ProcessingColorSpace);
- chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
- drawingChunks[chunkPos] = chunk;
- }
- var drawingChunk = drawingChunks[chunkPos];
- var referenceChunk = cache.GetChunk(chunkPos);
- // don't call floodfill if the chunk is empty
- if (referenceChunk.IsT1)
- {
- if (colorToReplace.A == 0 && !processedEmptyChunks.Contains(chunkPos))
- {
- drawingChunk.Surface.DrawingSurface.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)));
- }
- processedEmptyChunks.Add(chunkPos);
- }
- continue;
- }
- // use regular flood fill for chunks that have something in them
- var reallyReferenceChunk = referenceChunk.AsT0;
- var maybeArray = FloodFillChunk(
- reallyReferenceChunk,
- drawingChunk,
- selection,
- globalSelectionBounds,
- chunkPos,
- chunkSize,
- uLongColor,
- colorSpaceCorrectedColor,
- posOnChunk,
- colorRange,
- iter != 0);
- if (maybeArray is null)
- continue;
- for (int i = 0; i < chunkSize; i++)
- {
- if (chunkPos.Y > 0 && maybeArray[i] == Visited)
- positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
- if (chunkPos.Y < imageSizeInChunks.Y - 1 && maybeArray[chunkSize * (chunkSize - 1) + i] == Visited)
- positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
- if (chunkPos.X > 0 && maybeArray[i * chunkSize] == Visited)
- positionsToFloodFill.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
- if (chunkPos.X < imageSizeInChunks.X - 1 && maybeArray[i * chunkSize + (chunkSize - 1)] == Visited)
- positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
- }
- }
- return drawingChunks;
- }
- private static unsafe byte[]? FloodFillChunk(
- Chunk referenceChunk,
- Chunk drawingChunk,
- VectorPath? selection,
- RectI globalSelectionBounds,
- VecI chunkPos,
- int chunkSize,
- ulong colorBits,
- Color color,
- VecI pos,
- ColorBounds bounds,
- bool checkFirstPixel)
- {
- // color should be a fixed color
- if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
- return null;
- if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
- return null;
- byte[] pixelStates = new byte[chunkSize * chunkSize];
- DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);
- using var refPixmap = referenceChunk.Surface.DrawingSurface.PeekPixels();
- Half* refArray = (Half*)refPixmap.GetPixels();
- using var drawPixmap = drawingChunk.Surface.DrawingSurface.PeekPixels();
- Half* drawArray = (Half*)drawPixmap.GetPixels();
- Stack<VecI> toVisit = new();
- toVisit.Push(pos);
- while (toVisit.Count > 0)
- {
- VecI curPos = toVisit.Pop();
- int pixelOffset = curPos.X + curPos.Y * chunkSize;
- Half* drawPixel = drawArray + pixelOffset * 4;
- Half* refPixel = refArray + pixelOffset * 4;
- *(ulong*)drawPixel = colorBits;
- pixelStates[pixelOffset] = Visited;
- 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 &&
- bounds.IsWithinBounds(refPixel + 4))
- toVisit.Push(new(curPos.X + 1, curPos.Y));
- 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 &&
- bounds.IsWithinBounds(refPixel + 4 * chunkSize))
- toVisit.Push(new(curPos.X, curPos.Y + 1));
- }
- return pixelStates;
- }
- public static Surface FillSelection(IReadOnlyDocument document, VectorPath selection)
- {
- Surface surface = new Surface(document.Size);
- var inverse = new VectorPath();
- inverse.AddRect((RectD)new RectI(new(0, 0), document.Size));
- surface.DrawingSurface.Canvas.Clear(new Color(255, 255, 255, 255));
- surface.DrawingSurface.Canvas.Flush();
- surface.DrawingSurface.Canvas.ClipPath(inverse.Op(selection, VectorPathOp.Difference));
- surface.DrawingSurface.Canvas.Clear(new Color(0, 0, 0, 0));
- surface.DrawingSurface.Canvas.Flush();
- return surface;
- }
- /// <summary>
- /// Use skia to set all pixels in array that are inside selection to InSelection
- /// </summary>
- private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos,
- int chunkSize)
- {
- if (selection is null)
- {
- selection = new VectorPath();
- selection.AddRect((RectD)globalBounds);
- }
- RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
- if (localBounds.IsZeroOrNegativeArea)
- return;
- VectorPath shiftedSelection = new VectorPath(selection);
- shiftedSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
- fixed (byte* arr = array)
- {
- using DrawingSurface drawingSurface = DrawingSurface.Create(
- new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr,
- chunkSize);
- drawingSurface.Canvas.ClipPath(shiftedSelection);
- drawingSurface.Canvas.Clear(new Color(InSelection, InSelection, InSelection));
- drawingSurface.Canvas.Flush();
- }
- }
- }
|