Browse Source

Fix contour closing bug in contour building by rewriting it

Equbuxu 2 years ago
parent
commit
20b1a04561

+ 108 - 93
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -1,6 +1,4 @@
 using System.Collections;
-using System.Numerics;
-using System.Runtime.CompilerServices;
 using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Debugging;
@@ -16,7 +14,7 @@ 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);
@@ -138,7 +136,7 @@ public static class FloodFillHelper
         }
         return drawingChunks;
     }
-    
+
     private static unsafe byte[]? FloodFillChunk(
         Chunk referenceChunk,
         Chunk drawingChunk,
@@ -213,15 +211,15 @@ public static class FloodFillHelper
             drawingSurface.Canvas.Flush();
         }
     }
-    
+
     private static MagicWandVisualizer visualizer = new MagicWandVisualizer(Path.Combine("Debugging", "MagicWand"));
 
     public static VectorPath GetFloodFillSelection(VecI startingPos, HashSet<Guid> membersToFloodFill,
         IReadOnlyDocument document)
     {
-        if(startingPos.X < 0 || startingPos.Y < 0 || startingPos.X >= document.Size.X || startingPos.Y >= document.Size.Y)
+        if (startingPos.X < 0 || startingPos.Y < 0 || startingPos.X >= document.Size.X || startingPos.Y >= document.Size.Y)
             return new VectorPath();
-        
+
         int chunkSize = ChunkResolution.Full.PixelSize();
 
         FloodFillChunkCache cache = CreateCache(membersToFloodFill, document);
@@ -229,13 +227,13 @@ public static class FloodFillHelper
         VecI initChunkPos = OperationHelper.GetChunkPos(startingPos, chunkSize);
         VecI imageSizeInChunks = (VecI)(document.Size / (double)chunkSize).Ceiling();
         VecI initPosOnChunk = startingPos - initChunkPos * chunkSize;
-        
+
 
         Color colorToReplace = cache.GetChunk(initChunkPos).Match(
             (Chunk chunk) => chunk.Surface.GetSRGBPixel(initPosOnChunk),
             static (EmptyChunk _) => Colors.Transparent
         );
-        
+
         ColorBounds colorRange = new(colorToReplace);
 
         HashSet<VecI> processedEmptyChunks = new();
@@ -244,7 +242,7 @@ public static class FloodFillHelper
         positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
 
         Lines lines = new();
-        
+
         VectorPath selection = new();
         while (positionsToFloodFill.Count > 0)
         {
@@ -275,10 +273,10 @@ public static class FloodFillHelper
 
             // use regular flood fill for chunks that have something in them
             var reallyReferenceChunk = referenceChunk.AsT0;
-            
+
             VecI globalPos = chunkPos * chunkSize + posOnChunk;
-            
-            if(processedPositions.Contains(globalPos))
+
+            if (processedPositions.Contains(globalPos))
                 continue;
 
             visualizer.CurrentContext = $"FloodFill_{chunkPos}";
@@ -289,7 +287,7 @@ public static class FloodFillHelper
                 document.Size,
                 posOnChunk,
                 colorRange, lines, processedPositions);
-            
+
             if (maybeArray is null)
                 continue;
             for (int i = 0; i < chunkSize; i++)
@@ -309,7 +307,7 @@ public static class FloodFillHelper
         {
             selection = BuildContour(lines);
         }
-        
+
         visualizer.GenerateVisualization(document.Size.X, document.Size.Y, 500, 500);
 
         return selection;
@@ -320,7 +318,7 @@ public static class FloodFillHelper
     {
         bool isEdgeChunk = chunkPos.X == 0 || chunkPos.Y == 0 || chunkPos.X == imageSizeInChunks.X - 1 ||
                            chunkPos.Y == imageSizeInChunks.Y - 1;
-        
+
         visualizer.CurrentContext = "EmptyChunk";
         if (isEdgeChunk)
         {
@@ -337,7 +335,7 @@ public static class FloodFillHelper
 
             endX = Math.Clamp(endX, 0, realSize.X);
             endY = Math.Clamp(endY, 0, realSize.Y);
-            
+
 
             if (isTopEdge)
             {
@@ -353,7 +351,7 @@ public static class FloodFillHelper
             {
                 AddLine(new(new(posX, endY), new(posX, posY)), lines, Up);
             }
-            
+
             if (isRightEdge)
             {
                 AddLine(new(new(endX, posY), new(endX, endY)), lines, Down);
@@ -361,47 +359,42 @@ public static class FloodFillHelper
         }
     }
 
-    public static VectorPath BuildContour(Lines lines)
+    private static VecI GoStraight(Lines allLines, Line firstLine)
     {
-        VectorPath selection = new();
+        Line previous = default;
+        Line? current = firstLine;
+        while (current != null)
+        {
+            (previous, current) = ((Line)current, allLines.RemoveLineAt(current.Value.End, current.Value.NormalizedDirection));
+        }
 
-        List<Line> remainingLines = lines.ToList(); // I'm not sure how to avoid this yet
+        return previous.End;
+    }
 
-        Line startingLine = remainingLines[0];
-        VecI prevPos = startingLine.End;
-        VecI prevDir = startingLine.NormalizedDirection;
-        selection.MoveTo(startingLine.Start);
-        selection.LineTo(startingLine.End);
-        lines.RemoveLine(startingLine);
-        for (var i = 1; i < remainingLines.Count; i++)
+    private static void FollowPath(Lines allLines, Line startingLine, VectorPath path)
+    {
+        path.MoveTo(startingLine.Start);
+
+        Line? current = startingLine;
+        while (current != null)
         {
-            var line = remainingLines[i];
-            Line nextLine;
-            if (!lines.TryGetLine(prevPos, prevDir, out nextLine))
-            {
-                nextLine = line;
-                selection.MoveTo(nextLine.Start);
-                prevPos = nextLine.End;
-                prevDir = nextLine.NormalizedDirection;
-                lines.RemoveLine(nextLine);
-                remainingLines.RemoveAt(i);
-                i--;
-                continue;
-            }
+            VecI straightPathEnd = GoStraight(allLines, (Line)current);
+            path.LineTo(straightPathEnd);
+            current = allLines.RemoveLineAt(straightPathEnd);
+        }
+    }
 
-            if (prevDir != nextLine.NormalizedDirection)
-            {
-                selection.LineTo(prevPos);
-            }
+    private static VectorPath BuildContour(Lines lines)
+    {
+        VectorPath selection = new();
 
-            prevDir = nextLine.NormalizedDirection;
-            prevPos = nextLine.End;
-            lines.RemoveLine(nextLine);
-            remainingLines.Remove(nextLine);
-            i--;
+        Line? current = lines.PopLine();
+        while (current != null)
+        {
+            FollowPath(lines, (Line)current, selection);
+            current = lines.PopLine();
         }
-        
-        selection.Close();
+
         return selection;
     }
 
@@ -417,12 +410,12 @@ public static class FloodFillHelper
         {
             return null;
         }
-        
+
         byte[] pixelStates = new byte[chunkSize * chunkSize];
 
         using var refPixmap = referenceChunk.Surface.DrawingSurface.PeekPixels();
         Half* refArray = (Half*)refPixmap.GetPixels();
-        
+
         Stack<VecI> toVisit = new();
         toVisit.Push(pos);
 
@@ -439,18 +432,18 @@ public static class FloodFillHelper
                 processedPositions.Add(globalPos);
                 continue;
             }
-            
+
             pixelStates[pixelOffset] = Visited;
-            
+
             visualizer.CurrentContext = "AddCornerLines";
             AddCornerLines(documentSize, chunkOffset, lines, curPos, chunkSize, processedPositions);
-            
+
             visualizer.CurrentContext = "AddFillContourLines";
             AddFillContourLines(chunkSize, chunkOffset, bounds, lines, curPos, pixelStates, pixelOffset, refPixel, toVisit, globalPos, documentSize, processedPositions);
-            
+
             processedPositions.Add(globalPos);
         }
-        
+
         return pixelStates;
     }
 
@@ -458,9 +451,9 @@ public static class FloodFillHelper
         VecI curPos, byte[] pixelStates, int pixelOffset, Half* refPixel, Stack<VecI> toVisit, VecI globalPos,
         VecI documentSize, HashSet<VecI> processedPositions)
     {
-        
-        if(processedPositions.Contains(globalPos)) return;
-        
+
+        if (processedPositions.Contains(globalPos)) return;
+
         // Left pixel
         if (curPos.X > 0 && pixelStates[pixelOffset - 1] != Visited)
         {
@@ -568,9 +561,9 @@ public static class FloodFillHelper
     private static void AddLine(Line line, Lines lines, VecI direction)
     {
         VecI calculatedDir = (VecI)(line.End - line.Start).Normalized();
-        
+
         // if line in opposite direction exists, remove it
-        
+
         if (lines.TryCancelLine(line, direction))
         {
             visualizer.Steps.Add(new Step(line, StepType.CancelLine));
@@ -582,7 +575,7 @@ public static class FloodFillHelper
             lines.LineDict[direction][line.Start] = line;
             visualizer.Steps.Add(new Step(line));
         }
-        else if(calculatedDir == -direction)
+        else if (calculatedDir == -direction)
         {
             Line fixedLine = new Line(line.End, line.Start);
             lines.LineDict[direction][line.End] = fixedLine;
@@ -595,7 +588,7 @@ public static class FloodFillHelper
         }
     }
 
-    public struct Line
+    internal struct Line
     {
         public bool Equals(Line other)
         {
@@ -622,7 +615,7 @@ public static class FloodFillHelper
             End = end;
             NormalizedDirection = (VecI)(end - start).Normalized();
         }
-        
+
         public Line Extended(VecI point)
         {
             VecI start = Start;
@@ -631,22 +624,35 @@ public static class FloodFillHelper
             if (point.Y < Start.Y) start.Y = point.Y;
             if (point.X > End.X) end.X = point.X;
             if (point.Y > End.Y) end.Y = point.Y;
-            
+
             return new Line(start, end);
         }
-        
+
         public static bool operator ==(Line a, Line b)
         {
             return a.Start == b.Start && a.End == b.End;
         }
-        
+
         public static bool operator !=(Line a, Line b)
         {
             return !(a == b);
         }
+
+        public override string ToString()
+        {
+            string dir = (NormalizedDirection.X, NormalizedDirection.Y) switch
+            {
+                ( > 0, _) => "Right",
+                ( < 0, _) => "Left",
+                (_, < 0) => "Up",
+                (_, > 0) => "Down",
+                _ => "Weird dir"
+            };
+            return $"{Start}, {dir}";
+        }
     }
 
-    public class Lines : IEnumerable<Line>
+    internal class Lines : IEnumerable<Line>
     {
         public Dictionary<VecI, Dictionary<VecI, Line>> LineDict { get; set; } = new Dictionary<VecI, Dictionary<VecI, Line>>();
 
@@ -657,26 +663,35 @@ public static class FloodFillHelper
             LineDict[Left] = new Dictionary<VecI, Line>();
             LineDict[Up] = new Dictionary<VecI, Line>();
         }
-        
-        public bool TryGetLine(VecI start, VecI preferredDir, out Line line)
+
+        public Line? RemoveLineAt(VecI start)
+        {
+            foreach (var (_, lineDict) in LineDict)
+            {
+                if (lineDict.Remove(start, out Line value))
+                    return value;
+            }
+            return null;
+        }
+
+        public Line? RemoveLineAt(VecI start, VecI direction)
         {
-            if(LineDict[preferredDir].TryGetValue(start, out line)) return true;
+            if (LineDict[direction].Remove(start, out Line value))
+                return value;
+            return null;
+        }
 
-            VecI cachedOppositeDir = -preferredDir;
-            
-            foreach (var lineDict in LineDict.Values)
+        public Line? PopLine()
+        {
+            foreach (var (_, lineDict) in LineDict)
             {
-                // Preferred was already checked, opposite is invalid
-                if(lineDict == LineDict[preferredDir] || lineDict == LineDict[cachedOppositeDir]) continue;
-                
-                if (lineDict.TryGetValue(start, out line))
-                {
-                    return true;
-                }
+                if (lineDict.Count == 0)
+                    continue;
+                var result = lineDict.First();
+                lineDict.Remove(result.Key);
+                return result.Value;
             }
-            
-            line = default;
-            return false;
+            return null;
         }
 
         public IEnumerator<Line> GetEnumerator()
@@ -685,17 +700,17 @@ public static class FloodFillHelper
             {
                 yield return upLines.Value;
             }
-            
+
             foreach (var rightLines in LineDict[Right])
             {
                 yield return rightLines.Value;
             }
-            
+
             foreach (var downLines in LineDict[Down])
             {
                 yield return downLines.Value;
             }
-            
+
             foreach (var leftLines in LineDict[Left])
             {
                 yield return leftLines.Value;
@@ -712,7 +727,7 @@ public static class FloodFillHelper
             foreach (var lineDict in LineDict.Values)
             {
                 VecI dictDir = lineDict == LineDict[Up] ? Up : lineDict == LineDict[Right] ? Right : lineDict == LineDict[Down] ? Down : Left;
-                if(line.NormalizedDirection != dictDir) continue;
+                if (line.NormalizedDirection != dictDir) continue;
                 lineDict.Remove(line.Start);
             }
         }
@@ -720,14 +735,14 @@ public static class FloodFillHelper
         public bool TryCancelLine(Line line, VecI direction)
         {
             bool cancelingLineExists = false;
-            
+
             LineDict[-direction].TryGetValue(line.End, out Line cancelingLine);
             if (cancelingLine != default && cancelingLine.End == line.Start)
             {
                 cancelingLineExists = true;
                 LineDict[-direction].Remove(line.End);
             }
-            
+
             LineDict[direction].TryGetValue(line.Start, out cancelingLine);
             if (cancelingLine != default && cancelingLine.End == line.End)
             {

+ 15 - 15
src/PixiEditor.ChangeableDocument/Debugging/MagicWandVisualizer.cs

@@ -7,23 +7,23 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace PixiEditor.ChangeableDocument.Debugging;
 
-public class MagicWandVisualizer
+internal class MagicWandVisualizer
 {
     public string CurrentContext { get; set; } = "";
     public string FilePath { get; }
     private Paint drawingPaint;
     private Paint replacementPaint;
     public List<Step> Steps = new List<Step>();
-    
+
     public MagicWandVisualizer(string filePath)
     {
         drawingPaint = new Paint();
         drawingPaint.BlendMode = BlendMode.Src;
-        
+
         replacementPaint = new Paint();
         replacementPaint.BlendMode = BlendMode.Src;
         replacementPaint.ColorFilter = ColorFilter.CreateBlendMode(Colors.Black, BlendMode.ColorBurn);
-        
+
         FilePath = filePath;
         if (!Directory.Exists(filePath))
         {
@@ -50,17 +50,17 @@ public class MagicWandVisualizer
 
             var scaledStart = new VecI(step.Start.X * (width / originalWidth), step.Start.Y * (height / originalHeight));
             var scaledEnd = new VecI(step.End.X * (width / originalWidth), step.End.Y * (height / originalHeight));
-            
+
             DrawArrow(surface, scaledStart, scaledEnd, 2, step.Type == StepType.Add ? Colors.Green : Colors.Red);
-            
+
             using (FileStream stream = new FileStream(Path.Join(FilePath, $"Frame {i}_{CurrentContext}.png"), FileMode.Create))
             {
                 surface.Snapshot().Encode().SaveTo(stream);
             }
-            
+
             previousImage = surface.Snapshot();
         }
-        
+
         Steps.Clear();
     }
 
@@ -69,26 +69,26 @@ public class MagicWandVisualizer
         drawingPaint.Color = color;
         drawingPaint.StrokeWidth = thickness;
         surface.Canvas.DrawLine(start, end, drawingPaint);
-        
+
         // Draw arrow head
-        
+
         VecI direction = end - start;
         VecI perpendicular = new VecI(-direction.Y, direction.X);
-        
+
         VecI arrowHead1 = end - (VecI)direction.Normalized() * 10 + (VecI)perpendicular.Normalized() * 5;
         VecI arrowHead2 = end - (VecI)direction.Normalized() * 10 - (VecI)perpendicular.Normalized() * 5;
-        
+
         surface.Canvas.DrawLine(end, arrowHead1, drawingPaint);
         surface.Canvas.DrawLine(end, arrowHead2, drawingPaint);
     }
 }
 
-public class Step
+internal class Step
 {
     public VecI Start { get; set; }
     public VecI End { get; set; }
     public StepType Type { get; set; }
-    
+
     public Step(FloodFillHelper.Line line, StepType type = StepType.Add)
     {
         Start = line.Start;
@@ -97,7 +97,7 @@ public class Step
     }
 }
 
-public enum StepType
+internal enum StepType
 {
     Add,
     CancelLine