flabbet пре 2 година
родитељ
комит
ad40a0b90f

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

@@ -205,4 +205,124 @@ internal static class FloodFillHelper
             drawingSurface.Canvas.Flush();
         }
     }
+
+    public static VectorPath GetFloodFillSelection(VecI startingPos, HashSet<Guid> membersToFloodFill,
+        IReadOnlyDocument document)
+    {
+        int chunkSize = ChunkResolution.Full.PixelSize();
+
+        FloodFillChunkCache cache = CreateCache(membersToFloodFill, document);
+
+        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);
+
+        Dictionary<VecI, Chunk> drawingChunks = new();
+        HashSet<VecI> processedEmptyChunks = new();
+        Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
+        positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
+        
+        VectorPath selection = new();
+        selection.AddRect(new RectI(startingPos, VecI.One));
+        while (positionsToFloodFill.Count > 0)
+        {
+            var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
+
+            if (!drawingChunks.ContainsKey(chunkPos))
+            {
+                var chunk = Chunk.Create();
+                drawingChunks[chunkPos] = chunk;
+            }
+            var referenceChunk = cache.GetChunk(chunkPos);
+
+            // don't call floodfill if the chunk is empty
+            if (referenceChunk.IsT1)
+            {
+                if (colorToReplace.A == 0 && !processedEmptyChunks.Contains(chunkPos))
+                {
+                    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 = GetChunkFloodFill(
+                reallyReferenceChunk,
+                chunkSize,
+                posOnChunk,
+                colorRange, ref selection);
+
+            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 selection;
+    }
+
+    private static unsafe byte[]? GetChunkFloodFill(
+        Chunk referenceChunk,
+        int chunkSize,
+        VecI pos,
+        ColorBounds bounds, ref VectorPath selection)
+    {
+
+        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);
+
+        while (toVisit.Count > 0)
+        {
+            VecI curPos = toVisit.Pop();
+            int pixelOffset = curPos.X + curPos.Y * chunkSize;
+            Half* refPixel = refArray + pixelOffset * 4;
+            pixelStates[pixelOffset] = Visited;
+
+            if (curPos.X > 0 && pixelStates[pixelOffset - 1] != Visited && bounds.IsWithinBounds(refPixel - 4))
+                toVisit.Push(new(curPos.X - 1, curPos.Y));
+            if (curPos.X < chunkSize - 1 && pixelStates[pixelOffset + 1] != Visited && bounds.IsWithinBounds(refPixel + 4))
+                toVisit.Push(new(curPos.X + 1, curPos.Y));
+            if (curPos.Y > 0 && pixelStates[pixelOffset - chunkSize] != Visited && bounds.IsWithinBounds(refPixel - 4 * chunkSize))
+                toVisit.Push(new(curPos.X, curPos.Y - 1));
+            if (curPos.Y < chunkSize - 1 && pixelStates[pixelOffset + chunkSize] != Visited && bounds.IsWithinBounds(refPixel + 4 * chunkSize))
+                toVisit.Push(new(curPos.X, curPos.Y + 1));
+
+            selection.AddRect(new RectI(curPos, VecI.One));
+        }
+        
+        return pixelStates;
+    }
 }

+ 73 - 0
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand_UpdateableChange.cs

@@ -0,0 +1,73 @@
+using PixiEditor.ChangeableDocument.Changes.Drawing;
+using PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface.Vector;
+
+namespace PixiEditor.ChangeableDocument.Changes.Selection;
+
+internal class MagicWand_UpdateableChange : Change
+{
+    private VectorPath? originalPath;
+    private VectorPath path = new() { FillType = PathFillType.EvenOdd };
+    private VecI point;
+    private readonly Guid memberGuid;
+    private readonly bool referenceAll;
+    private readonly bool drawOnMask;
+    private readonly SelectionMode mode;
+    
+    [GenerateMakeChangeAction]
+    public MagicWand_UpdateableChange(Guid memberGuid, VecI point, SelectionMode mode, bool referenceAll, bool drawOnMask)
+    {
+        path.MoveTo(point);
+        this.mode = mode;
+        this.memberGuid = memberGuid;
+        this.referenceAll = referenceAll;
+        this.drawOnMask = drawOnMask;
+        this.point = point;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        originalPath = new VectorPath(target.Selection.SelectionPath);
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
+
+        HashSet<Guid> membersToReference = new();
+        if (referenceAll)
+            target.ForEveryReadonlyMember(member => membersToReference.Add(member.GuidValue));
+        else
+            membersToReference.Add(memberGuid);
+        path = FloodFillHelper.GetFloodFillSelection(point, membersToReference, target);
+        
+        ignoreInUndo = false;
+        return CommonApply(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return new None();
+    }
+    
+    private Selection_ChangeInfo CommonApply(Document target)
+    {
+        var toDispose = target.Selection.SelectionPath;
+        if (mode == SelectionMode.New)
+        {
+            var copy = new VectorPath(path);
+            copy.Close();
+            target.Selection.SelectionPath = copy;
+        }
+        else
+        {
+            target.Selection.SelectionPath = originalPath!.Op(path, mode.ToVectorPathOp());
+        }
+        toDispose.Dispose();
+
+        return new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath));
+    }
+}

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IVectorPathImplementation.cs

@@ -25,6 +25,7 @@ public interface IVectorPathImplementation
     public void Transform(VectorPath vectorPath, Matrix3X3 matrix);
     public RectD GetBounds(VectorPath vectorPath);
     public void Reset(VectorPath vectorPath);
+    public void AddRect(VectorPath path, RectI rect, PathDirection direction);
     public void MoveTo(VectorPath vectorPath, Point point);
     public void LineTo(VectorPath vectorPath, Point point);
     public void QuadTo(VectorPath vectorPath, Point mid, Point point);
@@ -34,4 +35,5 @@ public interface IVectorPathImplementation
     public VectorPath Op(VectorPath vectorPath, VectorPath ellipsePath, VectorPathOp pathOp);
     public void Close(VectorPath vectorPath);
     public string ToSvgPathData(VectorPath vectorPath);
+    public void AddPath(VectorPath vectorPath, VectorPath path, AddPathMode mode);
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Numerics/VecI.cs

@@ -15,6 +15,7 @@ public struct VecI : IEquatable<VecI>
     public int ShortestAxis => (Math.Abs(X) < Math.Abs(Y)) ? X : Y;
 
     public static VecI Zero { get; } = new(0, 0);
+    public static VecI One { get; } = new(1, 1);
 
     public VecI(int x, int y)
     {

+ 33 - 2
src/PixiEditor.DrawingApi.Core/Surface/Vector/VectorPath.cs

@@ -108,11 +108,20 @@ public class VectorPath : NativeObject
         DrawingBackendApi.Current.PathImplementation.AddOval(this, borders);
     }
 
-    public VectorPath Op(VectorPath ellipsePath, VectorPathOp pathOp)
+    /// <summary>
+    ///     Compute the result of a logical operation on two paths.
+    /// </summary>
+    /// <param name="other">Other path.</param>
+    /// <param name="pathOp">Logical operand.</param>
+    /// <returns>Returns the resulting path if the operation was successful, otherwise null.</returns>
+    public VectorPath Op(VectorPath other, VectorPathOp pathOp)
     {
-        return DrawingBackendApi.Current.PathImplementation.Op(this, ellipsePath, pathOp);
+        return DrawingBackendApi.Current.PathImplementation.Op(this, other, pathOp);
     }
 
+    /// <summary>
+    ///     Closes current contour.
+    /// </summary>
     public void Close()
     {
         DrawingBackendApi.Current.PathImplementation.Close(this);
@@ -122,4 +131,26 @@ public class VectorPath : NativeObject
     {
         return DrawingBackendApi.Current.PathImplementation.ToSvgPathData(this);
     }
+
+    public void AddRect(RectI rect, PathDirection direction = PathDirection.Clockwise)
+    {
+        DrawingBackendApi.Current.PathImplementation.AddRect(this, rect, direction);
+    }
+
+    public void AddPath(VectorPath path, AddPathMode mode)
+    {
+        DrawingBackendApi.Current.PathImplementation.AddPath(this, path, mode);
+    }
+}
+
+public enum PathDirection
+{
+    Clockwise,
+    CounterClockwise
+}
+
+public enum AddPathMode
+{
+    Append,
+    Extend
 }

+ 17 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPathImplementation.cs

@@ -107,6 +107,11 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances[vectorPath.ObjectPointer].Reset();
         }
 
+        public void AddRect(VectorPath path, RectI rect, PathDirection direction)
+        {
+            ManagedInstances[path.ObjectPointer].AddRect(rect.ToSkRect(), (SKPathDirection)direction);
+        }
+
         public void MoveTo(VectorPath vectorPath, Point point)
         {
             ManagedInstances[vectorPath.ObjectPointer].MoveTo(point.ToSkPoint());
@@ -136,7 +141,19 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
         {
             ManagedInstances[vectorPath.ObjectPointer].AddOval(borders.ToSkRect());
         }
+        
+        public void AddPath(VectorPath vectorPath, VectorPath other, AddPathMode mode)
+        {
+            ManagedInstances[vectorPath.ObjectPointer].AddPath(ManagedInstances[other.ObjectPointer], (SKPathAddMode)mode);
+        }
 
+        /// <summary>
+        ///     Compute the result of a logical operation on two paths.
+        /// </summary>
+        /// <param name="vectorPath">Source operand</param>
+        /// <param name="ellipsePath">The second operand.</param>
+        /// <param name="pathOp">The logical operator.</param>
+        /// <returns>Returns the resulting path if the operation was successful, otherwise null.h</returns>
         public VectorPath Op(VectorPath vectorPath, VectorPath ellipsePath, VectorPathOp pathOp)
         {
             SKPath skPath = ManagedInstances[vectorPath.ObjectPointer].Op(ManagedInstances[ellipsePath.ObjectPointer], (SKPathOp)pathOp);

+ 2 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -53,4 +53,6 @@ internal class DocumentToolsModule
     public void UseFloodFillTool() => Internals.ChangeController.TryStartExecutor<FloodFillToolExecutor>();
 
     public void UseLassoTool() => Internals.ChangeController.TryStartExecutor<LassoToolExecutor>();
+
+    public void UseMagicWandTool() => Internals.ChangeController.TryStartExecutor<MagicWandToolExecutor>();
 }

+ 50 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs

@@ -0,0 +1,50 @@
+using PixiEditor.ChangeableDocument.Actions.Undo;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+internal class MagicWandToolExecutor : UpdateableChangeExecutor
+{
+    private bool considerAllLayers;
+    private bool drawOnMask;
+    private Guid memberGuid;
+    private SelectionMode mode;
+
+    public override ExecutionState Start()
+    {
+        var magicWand = ViewModelMain.Current?.ToolsSubViewModel.GetTool<MagicWandToolViewModel>();
+        var member = document!.SelectedStructureMember;
+
+        if (magicWand is null || member is null)
+            return ExecutionState.Error;
+        drawOnMask = member is not LayerViewModel layer || layer.ShouldDrawOnMask;
+        if (drawOnMask && !member.HasMaskBindable)
+            return ExecutionState.Error;
+        if (!drawOnMask && member is not LayerViewModel)
+            return ExecutionState.Error;
+
+        mode = magicWand.SelectMode;
+        memberGuid = member.GuidValue;
+        considerAllLayers = magicWand.DocumentScope == DocumentScope.AllLayers;
+        var pos = controller!.LastPixelPosition;
+
+        internals!.ActionAccumulator.AddActions(new MagicWand_Action(memberGuid, pos, mode, considerAllLayers, drawOnMask));
+
+        return ExecutionState.Success;
+    }
+
+    public override void OnLeftMouseButtonUp()
+    {
+        internals!.ActionAccumulator.AddActions(new ChangeBoundary_Action());
+        onEnded!(this);
+    }
+
+    public override void ForceStop()
+    {
+        internals!.ActionAccumulator.AddActions(new ChangeBoundary_Action());
+    }
+}

+ 6 - 0
src/PixiEditor/ViewModels/SubViewModels/Tools/Tools/MagicWandToolViewModel.cs

@@ -1,5 +1,6 @@
 using System.Windows.Input;
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Toolbars;
@@ -25,4 +26,9 @@ internal class MagicWandToolViewModel : ToolViewModel
         Toolbar = ToolbarFactory.Create<MagicWandToolViewModel>();
         ActionDisplay = "Click to flood the selection.";
     }
+    
+    public override void OnLeftMouseButtonDown(VecD pos)
+    {
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseMagicWandTool();
+    }
 }