Browse Source

Selection refactoring, Selection modes, Lasso tool

Equbuxu 3 years ago
parent
commit
d9e7003da1

+ 46 - 12
src/ChunkyImageLib/ChunkyImage.cs

@@ -64,6 +64,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     private List<ChunkyImage> activeClips = new();
     private SKBlendMode blendMode = SKBlendMode.Src;
     private bool lockTransparency = false;
+    private SKPath? clippingPath;
     private int? horizontalSymmetryAxis = null;
     private int? verticalSymmetryAxis = null;
 
@@ -257,6 +258,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
+    public void SetClippingPath(SKPath clippingPath)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            if (queuedOperations.Count > 0)
+                throw new InvalidOperationException("This function can only be executed when there are no queued operations");
+            this.clippingPath = clippingPath;
+        }
+    }
+
     /// <summary>
     /// Porter duff compositing operators (apart from SrcOver) likely won't have the intended effect.
     /// </summary>
@@ -428,6 +440,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             lockTransparency = false;
             horizontalSymmetryAxis = null;
             verticalSymmetryAxis = null;
+            clippingPath = null;
 
             //clear latest chunks
             foreach (var (_, chunksOfRes) in latestChunks)
@@ -469,6 +482,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             lockTransparency = false;
             horizontalSymmetryAxis = null;
             verticalSymmetryAxis = null;
+            clippingPath = null;
 
             commitCounter++;
             if (commitCounter % 30 == 0)
@@ -634,7 +648,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             return;
 
         Chunk? targetChunk = null;
-        OneOf<FilledChunk, EmptyChunk, Chunk> combinedClips = new FilledChunk();
+        OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips = new FilledChunk();
 
         bool initialized = false;
 
@@ -648,11 +662,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             {
                 initialized = true;
                 targetChunk = GetOrCreateLatestChunk(chunkPos, resolution);
-                combinedClips = CombineClipsForChunk(chunkPos, resolution);
+                combinedRasterClips = CombineRasterClipsForChunk(chunkPos, resolution);
             }
 
             if (chunkData.QueueProgress <= i)
-                chunkData.IsDeleted = ApplyOperationToChunk(operation, combinedClips, targetChunk!, chunkPos, resolution, chunkData);
+                chunkData.IsDeleted = ApplyOperationToChunk(operation, combinedRasterClips, targetChunk!, chunkPos, resolution, chunkData);
         }
 
         if (initialized)
@@ -667,11 +681,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             latestChunksData[resolution][chunkPos] = chunkData;
         }
 
-        if (combinedClips.TryPickT2(out Chunk value, out var _))
+        if (combinedRasterClips.TryPickT2(out Chunk value, out var _))
             value.Dispose();
     }
 
-    private OneOf<FilledChunk, EmptyChunk, Chunk> CombineClipsForChunk(VecI chunkPos, ChunkResolution resolution)
+    private OneOf<FilledChunk, EmptyChunk, Chunk> CombineRasterClipsForChunk(VecI chunkPos, ChunkResolution resolution)
     {
         if (lockTransparency && MaybeGetCommittedChunk(chunkPos, ChunkResolution.Full) is null)
         {
@@ -705,7 +719,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     /// </returns>
     private bool ApplyOperationToChunk(
         IOperation operation,
-        OneOf<FilledChunk, EmptyChunk, Chunk> combinedClips,
+        OneOf<FilledChunk, EmptyChunk, Chunk> combinedRasterClips,
         Chunk targetChunk,
         VecI chunkPos,
         ChunkResolution resolution,
@@ -716,26 +730,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
 
         if (operation is IDrawOperation chunkOperation)
         {
-            if (combinedClips.IsT1) //Nothing is visible
+            if (combinedRasterClips.IsT1) //Nothing is visible
                 return chunkData.IsDeleted;
 
             if (chunkData.IsDeleted)
                 targetChunk.Surface.SkiaSurface.Canvas.Clear();
 
             // just regular drawing
-            if (combinedClips.IsT0) //Everything is visible
+            if (combinedRasterClips.IsT0) //Everything is visible as far as raster clips are concerned
             {
-                chunkOperation.DrawOnChunk(targetChunk, chunkPos);
+                CallDrawWithClip(chunkOperation, targetChunk, resolution, chunkPos);
                 return false;
             }
 
-            // drawing with clipping
-            var clip = combinedClips.AsT2;
+            // drawing with raster clipping
+            var clip = combinedRasterClips.AsT2;
 
             using var tempChunk = Chunk.Create(targetChunk.Resolution);
             targetChunk.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ReplacingPaint);
 
-            chunkOperation.DrawOnChunk(tempChunk, chunkPos);
+            CallDrawWithClip(chunkOperation, tempChunk, resolution, chunkPos);
 
             clip.DrawOnSurface(tempChunk.Surface.SkiaSurface, VecI.Zero, ClippingPaint);
             clip.DrawOnSurface(targetChunk.Surface.SkiaSurface, VecI.Zero, InverseClippingPaint);
@@ -751,6 +765,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         return chunkData.IsDeleted;
     }
 
+    private void CallDrawWithClip(IDrawOperation operation, Chunk targetChunk, ChunkResolution resolution, VecI chunkPos)
+    {
+        if (clippingPath is not null && !clippingPath.IsEmpty)
+        {
+            int count = targetChunk.Surface.SkiaSurface.Canvas.Save();
+
+            using SKPath transformedPath = new(clippingPath);
+            float scale = (float)resolution.Multiplier();
+            VecD trans = -chunkPos * FullChunkSize * scale;
+            transformedPath.Transform(SKMatrix.CreateScaleTranslation(scale, scale, (float)trans.X, (float)trans.Y));
+            targetChunk.Surface.SkiaSurface.Canvas.ClipPath(transformedPath);
+            operation.DrawOnChunk(targetChunk, chunkPos);
+            targetChunk.Surface.SkiaSurface.Canvas.RestoreToCount(count);
+        }
+        else
+        {
+            operation.DrawOnChunk(targetChunk, chunkPos);
+        }
+    }
+
     /// <summary>
     /// Finds and deletes empty committed chunks. Returns true if all existing chunks were deleted.
     /// Note: this function modifies the internal state, it is not thread safe! Use it only in changes (same as all the other functions that change the image in some way).

+ 1 - 4
src/PixiEditor.ChangeableDocument/ChangeInfos/Drawing/Selection_ChangeInfo.cs

@@ -1,8 +1,5 @@
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 
 public record class Selection_ChangeInfo : IChangeInfo
 {
-    public HashSet<VecI>? Chunks { get; init; }
 }

+ 1 - 4
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs

@@ -1,11 +1,8 @@
-using ChunkyImageLib;
-using SkiaSharp;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 public interface IReadOnlySelection
 {
-    public IReadOnlyChunkyImage SelectionImage { get; }
-    public bool IsEmptyAndInactive { get; }
     public SKPath SelectionPath { get; }
 }

+ 0 - 6
src/PixiEditor.ChangeableDocument/Changeables/Selection.cs

@@ -6,17 +6,11 @@ namespace PixiEditor.ChangeableDocument.Changeables;
 internal class Selection : IReadOnlySelection, IDisposable
 {
     public static SKColor SelectionColor { get; } = SKColors.CornflowerBlue;
-    public bool IsEmptyAndInactive { get; set; } = true;
-    public ChunkyImage SelectionImage { get; set; } = new(new(64, 64));
     public SKPath SelectionPath { get; set; } = new();
     SKPath IReadOnlySelection.SelectionPath => new SKPath(SelectionPath);
 
-    IReadOnlyChunkyImage IReadOnlySelection.SelectionImage => SelectionImage;
-    bool IReadOnlySelection.IsEmptyAndInactive => IsEmptyAndInactive;
-
     public void Dispose()
     {
-        SelectionImage.Dispose();
         SelectionPath.Dispose();
     }
 }

+ 4 - 22
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs

@@ -1,11 +1,9 @@
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
-using SkiaSharp;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal class ClearSelection_Change : Change
 {
-    private CommittedChunkStorage? savedSelection;
     private SKPath? originalPath;
 
     [GenerateMakeChangeAction]
@@ -13,47 +11,31 @@ internal class ClearSelection_Change : Change
 
     public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        if (target.Selection.IsEmptyAndInactive)
+        if (target.Selection.SelectionPath.IsEmpty)
             return new Error();
-        savedSelection = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAllChunks());
         originalPath = new SKPath(target.Selection.SelectionPath);
         return new Success();
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
     {
-        target.Selection.IsEmptyAndInactive = true;
-
-        target.Selection.SelectionImage.CancelChanges();
-        target.Selection.SelectionImage.EnqueueClear();
-        HashSet<VecI> affChunks = target.Selection.SelectionImage.FindAffectedChunks();
-        target.Selection.SelectionImage.CommitChanges();
-
         target.Selection.SelectionPath.Dispose();
         target.Selection.SelectionPath = new SKPath();
 
         ignoreInUndo = false;
-        return new Selection_ChangeInfo() { Chunks = affChunks };
+        return new Selection_ChangeInfo();
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        target.Selection.IsEmptyAndInactive = false;
-
-        target.Selection.SelectionImage.CancelChanges();
-        savedSelection!.ApplyChunksToImage(target.Selection.SelectionImage);
-        HashSet<VecI> affChunks = target.Selection.SelectionImage.FindAffectedChunks();
-        target.Selection.SelectionImage.CommitChanges();
-
         target.Selection.SelectionPath.Dispose();
         target.Selection.SelectionPath = new SKPath(originalPath);
 
-        return new Selection_ChangeInfo() { Chunks = affChunks };
+        return new Selection_ChangeInfo();
     }
 
     public override void Dispose()
     {
-        savedSelection?.Dispose();
         originalPath?.Dispose();
     }
 }

+ 3 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal static class DrawingChangeHelper
 {
     public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask)
@@ -21,8 +19,8 @@ internal static class DrawingChangeHelper
 
     public static void ApplyClipsSymmetriesEtc(Document target, ChunkyImage targetImage, Guid targetMemberGuid, bool drawOnMask)
     {
-        if (!target.Selection.IsEmptyAndInactive)
-            targetImage.AddRasterClip(target.Selection.SelectionImage);
+        if (!target.Selection.SelectionPath.IsEmpty)
+            targetImage.SetClippingPath(target.Selection.SelectionPath);
 
         var targetMember = target.FindMemberOrThrow(targetMemberGuid);
         if (targetMember is Layer layer && layer.LockTransparency && !drawOnMask)

+ 0 - 88
src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs

@@ -1,88 +0,0 @@
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
-using SkiaSharp;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-
-internal class SelectRectangle_UpdateableChange : UpdateableChange
-{
-    private bool originalIsEmpty;
-    private VecI pos;
-    private VecI size;
-    private CommittedChunkStorage? originalSelectionState;
-    private SKPath? originalPath;
-
-    [GenerateUpdateableChangeActions]
-    public SelectRectangle_UpdateableChange(VecI pos, VecI size)
-    {
-        Update(pos, size);
-    }
-    public override OneOf<Success, Error> InitializeAndValidate(Document target)
-    {
-        originalIsEmpty = target.Selection.IsEmptyAndInactive;
-        originalPath = new SKPath(target.Selection.SelectionPath);
-        return new Success();
-    }
-
-    [UpdateChangeMethod]
-    public void Update(VecI pos, VecI size)
-    {
-        this.pos = pos;
-        this.size = size;
-    }
-
-    private (Selection_ChangeInfo info, HashSet<VecI> chunks) CommonApply(Document target)
-    {
-        var oldChunks = target.Selection.SelectionImage.FindAffectedChunks();
-        target.Selection.SelectionImage.CancelChanges();
-        target.Selection.IsEmptyAndInactive = false;
-        target.Selection.SelectionImage.EnqueueDrawRectangle(new ShapeData(pos + size / 2, size, 0, 0, SKColors.Transparent, Selection.SelectionColor));
-
-        using SKPath rect = new SKPath();
-        rect.MoveTo(pos);
-        rect.LineTo(pos.X + size.X, pos.Y);
-        rect.LineTo(pos + size);
-        rect.LineTo(pos.X, pos.Y + size.Y);
-        rect.LineTo(pos);
-
-        target.Selection.SelectionPath.Dispose();
-        target.Selection.SelectionPath = originalPath!.Op(rect, SKPathOp.Union);
-
-        oldChunks.UnionWith(target.Selection.SelectionImage.FindAffectedChunks());
-        return (new Selection_ChangeInfo() { Chunks = oldChunks }, oldChunks);
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        var (info, _) = CommonApply(target);
-        return info;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
-    {
-        var (changes, affChunks) = CommonApply(target);
-        originalSelectionState = new CommittedChunkStorage(target.Selection.SelectionImage, affChunks);
-        target.Selection.SelectionImage.CommitChanges();
-        target.Selection.IsEmptyAndInactive = target.Selection.SelectionImage.CheckIfCommittedIsEmpty();
-        ignoreInUndo = false;
-        return changes;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
-    {
-        target.Selection.IsEmptyAndInactive = originalIsEmpty;
-        originalSelectionState!.ApplyChunksToImage(target.Selection.SelectionImage);
-        originalSelectionState.Dispose();
-        originalSelectionState = null;
-        var changes = new Selection_ChangeInfo() { Chunks = target.Selection.SelectionImage.FindAffectedChunks() };
-        target.Selection.SelectionImage.CommitChanges();
-        target.Selection.SelectionPath.Dispose();
-        target.Selection.SelectionPath = new SKPath(originalPath);
-        return changes;
-    }
-
-    public override void Dispose()
-    {
-        originalSelectionState?.Dispose();
-        originalPath?.Dispose();
-    }
-}

+ 0 - 13
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -9,7 +9,6 @@ internal class ResizeCanvas_Change : Change
     private int originalVerAxisX;
     private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
-    private CommittedChunkStorage? selectionChunkStorage;
     private VecI newSize;
 
     [GenerateMakeChangeAction]
@@ -59,11 +58,6 @@ internal class ResizeCanvas_Change : Change
             deletedMaskChunks.Add(layer.GuidValue, new CommittedChunkStorage(layer.Mask, layer.Mask.FindAffectedChunks()));
             layer.Mask.CommitChanges();
         });
-
-        target.Selection.SelectionImage.EnqueueResize(newSize);
-        selectionChunkStorage = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAffectedChunks());
-        target.Selection.SelectionImage.CommitChanges();
-
         ignoreInUndo = false;
         return new Size_ChangeInfo();
     }
@@ -85,12 +79,6 @@ internal class ResizeCanvas_Change : Change
             layer.Mask.CommitChanges();
         });
 
-        target.Selection.SelectionImage.EnqueueResize(originalSize);
-        selectionChunkStorage!.ApplyChunksToImage(target.Selection.SelectionImage);
-        target.Selection.SelectionImage.CommitChanges();
-        selectionChunkStorage.Dispose();
-        selectionChunkStorage = null;
-
         target.HorizontalSymmetryAxisY = originalHorAxisY;
         target.VerticalSymmetryAxisX = originalVerAxisX;
 
@@ -107,6 +95,5 @@ internal class ResizeCanvas_Change : Change
             layer.Value.Dispose();
         foreach (var mask in deletedMaskChunks)
             mask.Value.Dispose();
-        selectionChunkStorage?.Dispose();
     }
 }

+ 71 - 0
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectLasso_UpdateableChange.cs

@@ -0,0 +1,71 @@
+using PixiEditor.ChangeableDocument.Enums;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Selection;
+internal class SelectLasso_UpdateableChange : UpdateableChange
+{
+    private SKPath? originalPath;
+    private SKPath path = new();
+    private readonly SelectionMode mode;
+
+    [GenerateUpdateableChangeActions]
+    public SelectLasso_UpdateableChange(VecI point, SelectionMode mode)
+    {
+        path.MoveTo(point);
+        this.mode = mode;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(VecI point)
+    {
+        path.LineTo(point);
+    }
+
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        originalPath = new SKPath(target.Selection.SelectionPath);
+        return new Success();
+    }
+
+    private Selection_ChangeInfo CommonApply(Document target)
+    {
+        var toDispose = target.Selection.SelectionPath;
+        if (mode == SelectionMode.New)
+        {
+            var copy = new SKPath(path);
+            copy.Close();
+            target.Selection.SelectionPath = copy;
+        }
+        else
+        {
+            target.Selection.SelectionPath = originalPath!.Op(path, mode.ToSKPathOp());
+        }
+        toDispose.Dispose();
+
+        return new Selection_ChangeInfo();
+    }
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        return CommonApply(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        return CommonApply(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var toDispose = target.Selection.SelectionPath;
+        target.Selection.SelectionPath = new(originalPath);
+        toDispose.Dispose();
+        return new Selection_ChangeInfo();
+    }
+
+    public override void Dispose()
+    {
+        originalPath?.Dispose();
+        path?.Dispose();
+    }
+}

+ 75 - 0
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectRectangle_UpdateableChange.cs

@@ -0,0 +1,75 @@
+using PixiEditor.ChangeableDocument.Enums;
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Changes.Selection;
+
+internal class SelectRectangle_UpdateableChange : UpdateableChange
+{
+    private VecI pos;
+    private VecI size;
+    private SKPath? originalPath;
+    private readonly SelectionMode mode;
+
+    [GenerateUpdateableChangeActions]
+    public SelectRectangle_UpdateableChange(VecI pos, VecI size, SelectionMode mode)
+    {
+        Update(pos, size);
+        this.mode = mode;
+    }
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        originalPath = new SKPath(target.Selection.SelectionPath);
+        return new Success();
+    }
+
+    [UpdateChangeMethod]
+    public void Update(VecI pos, VecI size)
+    {
+        this.pos = pos;
+        this.size = size;
+    }
+
+    private Selection_ChangeInfo CommonApply(Document target)
+    {
+        using var rect = new SKPath();
+        rect.MoveTo(pos);
+        rect.LineTo(pos.X + size.X, pos.Y);
+        rect.LineTo(pos + size);
+        rect.LineTo(pos.X, pos.Y + size.Y);
+        rect.LineTo(pos);
+
+        var toDispose = target.Selection.SelectionPath;
+        if (mode == SelectionMode.New)
+            target.Selection.SelectionPath = new(rect);
+        else
+            target.Selection.SelectionPath = originalPath!.Op(rect, mode.ToSKPathOp());
+        toDispose.Dispose();
+
+        return new Selection_ChangeInfo();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        return CommonApply(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, out bool ignoreInUndo)
+    {
+        var changes = CommonApply(target);
+        ignoreInUndo = false;
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var changes = new Selection_ChangeInfo();
+        target.Selection.SelectionPath.Dispose();
+        target.Selection.SelectionPath = new SKPath(originalPath);
+        return changes;
+    }
+
+    public override void Dispose()
+    {
+        originalPath?.Dispose();
+    }
+}

+ 5 - 0
src/PixiEditor.ChangeableDocument/Enums/SelectionMode.cs

@@ -0,0 +1,5 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+public enum SelectionMode
+{
+    New, Add, Subtract, Intersect
+}

+ 17 - 0
src/PixiEditor.ChangeableDocument/Enums/SelectionModeEx.cs

@@ -0,0 +1,17 @@
+using SkiaSharp;
+
+namespace PixiEditor.ChangeableDocument.Enums;
+internal static class SelectionModeEx
+{
+    public static SKPathOp ToSKPathOp(this SelectionMode mode)
+    {
+        return mode switch
+        {
+            SelectionMode.New => throw new ArgumentException("The New mode has no corresponding operation"),
+            SelectionMode.Add => SKPathOp.Union,
+            SelectionMode.Subtract => SKPathOp.Difference,
+            SelectionMode.Intersect => SKPathOp.Intersect,
+            _ => throw new NotImplementedException(),
+        };
+    }
+}

+ 2 - 1
src/PixiEditorPrototype/CustomControls/SelectionOverlay.cs

@@ -31,6 +31,7 @@ internal class SelectionOverlay : Control
 
     private Pen whitePen = new Pen(Brushes.White, 1);
     private Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = frame7 };
+    private Brush fillBrush = new SolidColorBrush(Color.FromArgb(80, 0, 80, 255));
 
     private static DashStyle frame1 = new DashStyle(new double[] { 2, 4 }, 0);
     private static DashStyle frame2 = new DashStyle(new double[] { 2, 4 }, 1);
@@ -76,7 +77,7 @@ internal class SelectionOverlay : Control
             Figures = (PathFigureCollection?)converter.ConvertFromString(Path.ToSvgPathData()),
         };
         drawingContext.DrawGeometry(null, whitePen, renderPath);
-        drawingContext.DrawGeometry(null, blackDashedPen, renderPath);
+        drawingContext.DrawGeometry(fillBrush, blackDashedPen, renderPath);
     }
 
     private static void OnZoomboxScaleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)

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

@@ -4,6 +4,7 @@ internal enum Tool
 {
     Rectangle,
     Select,
+    Lasso,
     ShiftLayer,
     FloodFill
 }

+ 24 - 9
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -150,6 +150,8 @@ internal class DocumentViewModel : INotifyPropertyChanged
 
     private bool updateableChangeActive = false;
 
+    private bool selectingRect = false;
+    private bool selectingLasso = false;
     private bool drawingRectangle = false;
     private bool transformingRectangle = false;
     private bool shiftingLayer = false;
@@ -217,6 +219,22 @@ internal class DocumentViewModel : INotifyPropertyChanged
         Helpers.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
     }
 
+    public void StartUpdateLassoSelection(VecI startPos, SelectionMode mode)
+    {
+        updateableChangeActive = true;
+        selectingLasso = true;
+        Helpers.ActionAccumulator.AddActions(new SelectLasso_Action(startPos, mode));
+    }
+
+    public void EndLassoSelection()
+    {
+        if (!selectingLasso)
+            return;
+        selectingLasso = false;
+        updateableChangeActive = false;
+        Helpers.ActionAccumulator.AddFinishedActions(new EndSelectLasso_Action());
+    }
+
     public void ApplyTransform(object? param)
     {
         if (!transformingRectangle && !pastingImage)
@@ -239,21 +257,18 @@ internal class DocumentViewModel : INotifyPropertyChanged
         updateableChangeActive = false;
     }
 
-    bool startedSelection = false;
-    public void StartUpdateSelection(VecI pos, VecI size)
+    public void StartUpdateRectSelection(VecI pos, VecI size, SelectionMode mode)
     {
-        //if (!startedSelection)
-        //   Helpers.ActionAccumulator.AddActions(new ClearSelection_Action());
-        startedSelection = true;
+        selectingRect = true;
         updateableChangeActive = true;
-        Helpers.ActionAccumulator.AddActions(new SelectRectangle_Action(pos, size));
+        Helpers.ActionAccumulator.AddActions(new SelectRectangle_Action(pos, size, mode));
     }
 
-    public void EndSelection()
+    public void EndRectSelection()
     {
-        if (!startedSelection)
+        if (!selectingRect)
             return;
-        startedSelection = false;
+        selectingRect = false;
         updateableChangeActive = false;
         Helpers.ActionAccumulator.AddFinishedActions(new EndSelectRectangle_Action());
     }

+ 27 - 7
src/PixiEditorPrototype/ViewModels/ViewModelMain.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Windows.Input;
 using System.Windows.Media;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Zoombox;
 using PixiEditorPrototype.Models;
 using SkiaSharp;
@@ -14,10 +15,11 @@ internal class ViewModelMain : INotifyPropertyChanged
 {
     public DocumentViewModel? ActiveDocument => GetDocumentByGuid(activeDocumentGuid);
 
-    public RelayCommand? MouseDownCommand { get; }
-    public RelayCommand? MouseMoveCommand { get; }
-    public RelayCommand? MouseUpCommand { get; }
-    public RelayCommand? ChangeActiveToolCommand { get; }
+    public RelayCommand MouseDownCommand { get; }
+    public RelayCommand MouseMoveCommand { get; }
+    public RelayCommand MouseUpCommand { get; }
+    public RelayCommand ChangeActiveToolCommand { get; }
+    public RelayCommand SetSelectionModeCommand { get; }
 
     public Color SelectedColor { get; set; } = Colors.Black;
 
@@ -68,6 +70,8 @@ internal class ViewModelMain : INotifyPropertyChanged
     private bool mouseIsDown = false;
 
     private Tool activeTool = Tool.Rectangle;
+    private SelectionMode selectionMode = SelectionMode.New;
+
     private Tool toolOnMouseDown = Tool.Rectangle;
 
     public ViewModelMain()
@@ -76,12 +80,20 @@ internal class ViewModelMain : INotifyPropertyChanged
         MouseMoveCommand = new RelayCommand(MouseMove);
         MouseUpCommand = new RelayCommand(MouseUp);
         ChangeActiveToolCommand = new RelayCommand(ChangeActiveTool);
+        SetSelectionModeCommand = new RelayCommand(SetSelectionMode);
 
         var doc = new DocumentViewModel(this);
         documents[doc.GuidValue] = doc;
         activeDocumentGuid = doc.GuidValue;
     }
 
+    private void SetSelectionMode(object? obj)
+    {
+        if (obj is not SelectionMode mode)
+            return;
+        selectionMode = mode;
+    }
+
     public DocumentViewModel? GetDocumentByGuid(Guid guid)
     {
         return documents.TryGetValue(guid, out DocumentViewModel? value) ? value : null;
@@ -146,14 +158,19 @@ internal class ViewModelMain : INotifyPropertyChanged
         }
         else if (toolOnMouseDown == Tool.Select)
         {
-            ActiveDocument!.StartUpdateSelection(
+            ActiveDocument!.StartUpdateRectSelection(
                 new(mouseDownCanvasX, mouseDownCanvasY),
-                new(canvasX - mouseDownCanvasX, canvasY - mouseDownCanvasY));
+                new(canvasX - mouseDownCanvasX, canvasY - mouseDownCanvasY),
+                selectionMode);
         }
         else if (toolOnMouseDown == Tool.ShiftLayer)
         {
             ActiveDocument!.StartUpdateShiftLayer(new(canvasX - mouseDownCanvasX, canvasY - mouseDownCanvasY));
         }
+        else if (toolOnMouseDown == Tool.Lasso)
+        {
+            ActiveDocument!.StartUpdateLassoSelection(new(canvasX, canvasY), selectionMode);
+        }
     }
 
     private void MouseUp(object? param)
@@ -175,11 +192,14 @@ internal class ViewModelMain : INotifyPropertyChanged
                     ActiveDocument!.EndRectangleDrawing();
                     break;
                 case Tool.Select:
-                    ActiveDocument!.EndSelection();
+                    ActiveDocument!.EndRectSelection();
                     break;
                 case Tool.ShiftLayer:
                     ActiveDocument!.EndShiftLayer();
                     break;
+                case Tool.Lasso:
+                    ActiveDocument!.EndLassoSelection();
+                    break;
             }
         }
 

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

@@ -13,6 +13,7 @@
         xmlns:vp="clr-namespace:PixiEditorPrototype.UserControls.Viewport"
         xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
         xmlns:models="clr-namespace:PixiEditorPrototype.Models"
+        xmlns:chen="clr-namespace:PixiEditor.ChangeableDocument.Enums;assembly=PixiEditor.ChangeableDocument"
         xmlns:colorpicker="clr-namespace:ColorPicker;assembly=ColorPicker"
         mc:Ignorable="d"
         Title="MainWindow" Height="700" Width="1400">
@@ -182,6 +183,17 @@
                     <Button Width="50" Margin="5" Command="{Binding ActiveDocument.UndoCommand}">Undo</Button>
                     <Button Width="50" Margin="5" Command="{Binding ActiveDocument.RedoCommand}">Redo</Button>
                     <Button Width="100" Margin="5" Command="{Binding ActiveDocument.ClearSelectionCommand}">Clear selection</Button>
+                    <ComboBox Width="50" Height="20" Margin="5" SelectedIndex="0" x:Name="selectionModeComboBox">
+                        <ComboBoxItem Tag="{x:Static chen:SelectionMode.New}">New</ComboBoxItem>
+                        <ComboBoxItem Tag="{x:Static chen:SelectionMode.Add}">Add</ComboBoxItem>
+                        <ComboBoxItem Tag="{x:Static chen:SelectionMode.Subtract}">Subtract</ComboBoxItem>
+                        <ComboBoxItem Tag="{x:Static chen:SelectionMode.Intersect}">Intersect</ComboBoxItem>
+                        <i:Interaction.Triggers>
+                            <i:EventTrigger EventName="SelectionChanged">
+                                <i:InvokeCommandAction Command="{Binding SetSelectionModeCommand}" CommandParameter="{Binding SelectedItem.Tag, ElementName=selectionModeComboBox}"/>
+                            </i:EventTrigger>
+                        </i:Interaction.Triggers>
+                    </ComboBox>
                     <Button Width="120" Margin="5" Command="{Binding ActiveDocument.ClearHistoryCommand}">Clear undo history</Button>
                     <Button Width="100" Margin="5" Command="{Binding ActiveDocument.PasteImageCommand}">Paste Image</Button>
                     <Button Width="100" Margin="5" Command="{Binding ActiveDocument.ApplyTransformCommand}">Apply Transform</Button>
@@ -197,6 +209,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.Lasso}">Lasso</Button>
                 <Button Width="50" Margin="5" Command="{Binding ChangeActiveToolCommand}" CommandParameter="{x:Static models:Tool.ShiftLayer}">Shift Layer</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"/>

+ 3 - 3
src/README.md

@@ -99,13 +99,13 @@ Decouples the state of a document from the UI.
         - [ ] Fill
         - [ ] Brightness
         - [x] Basic selection changes
-        - [ ] Selection modes
+        - [x] Selection modes
         - [ ] Circular selection
         - [ ] Magic wand
-        - [ ] Lasso
+        - [x] Lasso
         - [x] Shift layer image
         - [ ] Move/Rotate selection
-        - [ ] Transform selection
+        - [ ] Transform selected area
         - [ ] Fill selection (fill with tranparent = delete)
         - [x] Clip to selection
         - [x] Lock transparency