FloodFillHelper.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. using ChunkyImageLib.Operations;
  2. using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
  3. using PixiEditor.ChangeableDocument.Changeables.Interfaces;
  4. using Drawie.Backend.Core;
  5. using Drawie.Backend.Core.ColorsImpl;
  6. using Drawie.Backend.Core.Numerics;
  7. using Drawie.Backend.Core.Surfaces;
  8. using Drawie.Backend.Core.Surfaces.ImageData;
  9. using Drawie.Backend.Core.Vector;
  10. using Drawie.Numerics;
  11. namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
  12. public static class FloodFillHelper
  13. {
  14. private const byte InSelection = 1;
  15. private const byte Visited = 2;
  16. private static readonly VecI Up = new VecI(0, -1);
  17. private static readonly VecI Down = new VecI(0, 1);
  18. private static readonly VecI Left = new VecI(-1, 0);
  19. private static readonly VecI Right = new VecI(1, 0);
  20. internal static FloodFillChunkCache CreateCache(HashSet<Guid> membersToFloodFill, IReadOnlyDocument document,
  21. int frame)
  22. {
  23. if (membersToFloodFill.Count == 1)
  24. {
  25. Guid guid = membersToFloodFill.First();
  26. var member = document.FindMemberOrThrow(guid);
  27. if (member is IReadOnlyFolderNode)
  28. return new FloodFillChunkCache(membersToFloodFill, document, frame);
  29. if (member is not IReadOnlyImageNode rasterLayer)
  30. throw new InvalidOperationException("Member is not a raster layer");
  31. return new FloodFillChunkCache(rasterLayer.GetLayerImageAtFrame(frame));
  32. }
  33. return new FloodFillChunkCache(membersToFloodFill, document, frame);
  34. }
  35. public static Dictionary<VecI, Chunk> FloodFill(
  36. HashSet<Guid> membersToFloodFill,
  37. IReadOnlyDocument document,
  38. VectorPath? selection,
  39. VecI startingPos,
  40. Color drawingColor,
  41. float tolerance,
  42. int frame)
  43. {
  44. if (selection is not null && !selection.Contains(startingPos.X + 0.5f, startingPos.Y + 0.5f))
  45. return new();
  46. int chunkSize = ChunkResolution.Full.PixelSize();
  47. FloodFillChunkCache cache = CreateCache(membersToFloodFill, document, frame);
  48. VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
  49. VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
  50. VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
  51. var chunkAtPos = cache.GetChunk(initChunkPos);
  52. Color colorToReplace = chunkAtPos.Match(
  53. (Chunk chunk) => chunk.Surface.GetRawPixel(initPosOnChunk),
  54. static (EmptyChunk _) => Colors.Transparent
  55. );
  56. ulong uLongColor = drawingColor.ToULong();
  57. Color colorSpaceCorrectedColor = drawingColor;
  58. if (!document.ProcessingColorSpace.IsSrgb)
  59. {
  60. var srgbTransform = ColorSpace.CreateSrgb().GetTransformFunction();
  61. var fixedColor = drawingColor.TransformColor(srgbTransform);
  62. uLongColor = fixedColor.ToULong();
  63. colorSpaceCorrectedColor = fixedColor;
  64. }
  65. if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
  66. return new();
  67. RectI globalSelectionBounds = (RectI?)selection?.TightBounds ?? new RectI(VecI.Zero, document.Size);
  68. // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.
  69. // Used for faster pixel checking
  70. ColorBounds colorRange = new(colorToReplace, tolerance);
  71. Dictionary<VecI, Chunk> drawingChunks = new();
  72. HashSet<VecI> processedEmptyChunks = new();
  73. // flood fill chunks using a basic 4-way approach with a stack (each chunk is kinda like a pixel)
  74. // once the chunk is filled all places where it spills over to neighboring chunks are saved in the stack
  75. Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
  76. positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
  77. int iter = -1;
  78. while (positionsToFloodFill.Count > 0)
  79. {
  80. iter++;
  81. var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
  82. if (!drawingChunks.ContainsKey(chunkPos))
  83. {
  84. var chunk = Chunk.Create(document.ProcessingColorSpace);
  85. chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
  86. drawingChunks[chunkPos] = chunk;
  87. }
  88. var drawingChunk = drawingChunks[chunkPos];
  89. var referenceChunk = cache.GetChunk(chunkPos);
  90. // don't call floodfill if the chunk is empty
  91. if (referenceChunk.IsT1)
  92. {
  93. if (colorToReplace.A == 0 && !processedEmptyChunks.Contains(chunkPos))
  94. {
  95. drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
  96. for (int i = 0; i < chunkSize; i++)
  97. {
  98. if (chunkPos.Y > 0)
  99. positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
  100. if (chunkPos.Y < imageSizeInChunks.Y - 1)
  101. positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
  102. if (chunkPos.X > 0)
  103. positionsToFloodFill.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
  104. if (chunkPos.X < imageSizeInChunks.X - 1)
  105. positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
  106. }
  107. processedEmptyChunks.Add(chunkPos);
  108. }
  109. continue;
  110. }
  111. // use regular flood fill for chunks that have something in them
  112. var reallyReferenceChunk = referenceChunk.AsT0;
  113. var maybeArray = FloodFillChunk(
  114. reallyReferenceChunk,
  115. drawingChunk,
  116. selection,
  117. globalSelectionBounds,
  118. chunkPos,
  119. chunkSize,
  120. uLongColor,
  121. colorSpaceCorrectedColor,
  122. posOnChunk,
  123. colorRange,
  124. iter != 0);
  125. if (maybeArray is null)
  126. continue;
  127. for (int i = 0; i < chunkSize; i++)
  128. {
  129. if (chunkPos.Y > 0 && maybeArray[i] == Visited)
  130. positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
  131. if (chunkPos.Y < imageSizeInChunks.Y - 1 && maybeArray[chunkSize * (chunkSize - 1) + i] == Visited)
  132. positionsToFloodFill.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
  133. if (chunkPos.X > 0 && maybeArray[i * chunkSize] == Visited)
  134. positionsToFloodFill.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
  135. if (chunkPos.X < imageSizeInChunks.X - 1 && maybeArray[i * chunkSize + (chunkSize - 1)] == Visited)
  136. positionsToFloodFill.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
  137. }
  138. }
  139. return drawingChunks;
  140. }
  141. private static unsafe byte[]? FloodFillChunk(
  142. Chunk referenceChunk,
  143. Chunk drawingChunk,
  144. VectorPath? selection,
  145. RectI globalSelectionBounds,
  146. VecI chunkPos,
  147. int chunkSize,
  148. ulong colorBits,
  149. Color color,
  150. VecI pos,
  151. ColorBounds bounds,
  152. bool checkFirstPixel)
  153. {
  154. // color should be a fixed color
  155. if (referenceChunk.Surface.GetRawPixel(pos) == color || drawingChunk.Surface.GetRawPixel(pos) == color)
  156. return null;
  157. if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
  158. return null;
  159. byte[] pixelStates = new byte[chunkSize * chunkSize];
  160. DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);
  161. using var refPixmap = referenceChunk.Surface.DrawingSurface.PeekPixels();
  162. Half* refArray = (Half*)refPixmap.GetPixels();
  163. using var drawPixmap = drawingChunk.Surface.DrawingSurface.PeekPixels();
  164. Half* drawArray = (Half*)drawPixmap.GetPixels();
  165. Stack<VecI> toVisit = new();
  166. toVisit.Push(pos);
  167. while (toVisit.Count > 0)
  168. {
  169. VecI curPos = toVisit.Pop();
  170. int pixelOffset = curPos.X + curPos.Y * chunkSize;
  171. Half* drawPixel = drawArray + pixelOffset * 4;
  172. Half* refPixel = refArray + pixelOffset * 4;
  173. *(ulong*)drawPixel = colorBits;
  174. pixelStates[pixelOffset] = Visited;
  175. if (curPos.X > 0 && pixelStates[pixelOffset - 1] == InSelection && bounds.IsWithinBounds(refPixel - 4))
  176. toVisit.Push(new(curPos.X - 1, curPos.Y));
  177. if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] == InSelection &&
  178. bounds.IsWithinBounds(refPixel + 4))
  179. toVisit.Push(new(curPos.X + 1, curPos.Y));
  180. if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] == InSelection &&
  181. bounds.IsWithinBounds(refPixel - 4 * chunkSize))
  182. toVisit.Push(new(curPos.X, curPos.Y - 1));
  183. if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] == InSelection &&
  184. bounds.IsWithinBounds(refPixel + 4 * chunkSize))
  185. toVisit.Push(new(curPos.X, curPos.Y + 1));
  186. }
  187. return pixelStates;
  188. }
  189. public static Surface FillSelection(IReadOnlyDocument document, VectorPath selection)
  190. {
  191. Surface surface = new Surface(document.Size);
  192. var inverse = new VectorPath();
  193. inverse.AddRect((RectD)new RectI(new(0, 0), document.Size));
  194. surface.DrawingSurface.Canvas.Clear(new Color(255, 255, 255, 255));
  195. surface.DrawingSurface.Canvas.Flush();
  196. surface.DrawingSurface.Canvas.ClipPath(inverse.Op(selection, VectorPathOp.Difference));
  197. surface.DrawingSurface.Canvas.Clear(new Color(0, 0, 0, 0));
  198. surface.DrawingSurface.Canvas.Flush();
  199. return surface;
  200. }
  201. /// <summary>
  202. /// Use skia to set all pixels in array that are inside selection to InSelection
  203. /// </summary>
  204. private static unsafe void DrawSelection(byte[] array, VectorPath? selection, RectI globalBounds, VecI chunkPos,
  205. int chunkSize)
  206. {
  207. if (selection is null)
  208. {
  209. selection = new VectorPath();
  210. selection.AddRect((RectD)globalBounds);
  211. }
  212. RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
  213. if (localBounds.IsZeroOrNegativeArea)
  214. return;
  215. VectorPath shiftedSelection = new VectorPath(selection);
  216. shiftedSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
  217. fixed (byte* arr = array)
  218. {
  219. using DrawingSurface drawingSurface = DrawingSurface.Create(
  220. new ImageInfo(localBounds.Right, localBounds.Bottom, ColorType.Gray8, AlphaType.Opaque), (IntPtr)arr,
  221. chunkSize);
  222. drawingSurface.Canvas.ClipPath(shiftedSelection);
  223. drawingSurface.Canvas.Clear(new Color(InSelection, InSelection, InSelection));
  224. drawingSurface.Canvas.Flush();
  225. }
  226. }
  227. }