Преглед изворни кода

Animated border for the selection

Equbuxu пре 3 година
родитељ
комит
99e4acf3db

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlySelection.cs

@@ -1,4 +1,5 @@
 using ChunkyImageLib;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
@@ -6,4 +7,5 @@ public interface IReadOnlySelection
 {
     public IReadOnlyChunkyImage ReadOnlySelectionImage { get; }
     public bool ReadOnlyIsEmptyAndInactive { get; }
+    public SKPath ReadOnlySelectionPath { get; }
 }

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

@@ -9,6 +9,8 @@ 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();
+    public SKPath ReadOnlySelectionPath => new SKPath(SelectionPath);
 
     public IReadOnlyChunkyImage ReadOnlySelectionImage => SelectionImage;
     public bool ReadOnlyIsEmptyAndInactive => IsEmptyAndInactive;
@@ -16,5 +18,6 @@ internal class Selection : IReadOnlySelection, IDisposable
     public void Dispose()
     {
         SelectionImage.Dispose();
+        SelectionPath.Dispose();
     }
 }

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs

@@ -3,6 +3,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
@@ -10,11 +11,13 @@ internal class ClearSelection_Change : Change
 {
     private bool originalIsEmpty;
     private CommittedChunkStorage? savedSelection;
+    private SKPath? originalPath;
     public override void Initialize(Document target)
     {
         originalIsEmpty = target.Selection.IsEmptyAndInactive;
         if (!originalIsEmpty)
             savedSelection = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAllChunks());
+        originalPath = new SKPath(target.Selection.SelectionPath);
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
@@ -31,6 +34,9 @@ internal class ClearSelection_Change : Change
         HashSet<Vector2i> 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 };
     }
@@ -46,11 +52,15 @@ internal class ClearSelection_Change : Change
         HashSet<Vector2i> 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 };
     }
 
     public override void Dispose()
     {
         savedSelection?.Dispose();
+        originalPath?.Dispose();
     }
 }

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

@@ -13,6 +13,7 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
     private Vector2i pos;
     private Vector2i size;
     private CommittedChunkStorage? originalSelectionState;
+    private SKPath? originalPath;
     public SelectRectangle_UpdateableChange(Vector2i pos, Vector2i size)
     {
         Update(pos, size);
@@ -20,6 +21,7 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
     public override void Initialize(Document target)
     {
         originalIsEmpty = target.Selection.IsEmptyAndInactive;
+        originalPath = new SKPath(target.Selection.SelectionPath);
     }
 
     public void Update(Vector2i pos, Vector2i size)
@@ -35,6 +37,16 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
         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 };
     }
@@ -57,11 +69,14 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
         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();
     }
 }

+ 89 - 0
src/PixiEditorPrototype/CustomControls/SelectionOverlay.cs

@@ -0,0 +1,89 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using SkiaSharp;
+
+namespace PixiEditorPrototype.CustomControls;
+
+public class SelectionOverlay : Control
+{
+    public static readonly DependencyProperty PathProperty =
+        DependencyProperty.Register(nameof(Path), typeof(SKPath), typeof(SelectionOverlay),
+            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
+
+
+    public static readonly DependencyProperty ZoomboxScaleProperty =
+        DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(SelectionOverlay), new(1.0, OnZoomboxScaleChanged));
+
+    public double ZoomboxScale
+    {
+        get => (double)GetValue(ZoomboxScaleProperty);
+        set => SetValue(ZoomboxScaleProperty, value);
+    }
+
+    public SKPath? Path
+    {
+        get => (SKPath?)GetValue(PathProperty);
+        set => SetValue(PathProperty, value);
+    }
+
+    private Pen whitePen = new Pen(Brushes.White, 1);
+    private Pen blackDashedPen = new Pen(Brushes.Black, 1) { DashStyle = frame7 };
+
+    private static DashStyle frame1 = new DashStyle(new double[] { 2, 4 }, 0);
+    private static DashStyle frame2 = new DashStyle(new double[] { 2, 4 }, 1);
+    private static DashStyle frame3 = new DashStyle(new double[] { 2, 4 }, 2);
+    private static DashStyle frame4 = new DashStyle(new double[] { 2, 4 }, 3);
+    private static DashStyle frame5 = new DashStyle(new double[] { 2, 4 }, 4);
+    private static DashStyle frame6 = new DashStyle(new double[] { 2, 4 }, 5);
+    private static DashStyle frame7 = new DashStyle(new double[] { 2, 4 }, 6);
+
+    private Geometry renderPath = new PathGeometry();
+    private PathFigureCollectionConverter converter = new();
+
+    public SelectionOverlay()
+    {
+        IsHitTestVisible = false;
+
+        blackDashedPen.BeginAnimation(Pen.DashStyleProperty, new ObjectAnimationUsingKeyFrames()
+        {
+            KeyFrames = new ObjectKeyFrameCollection()
+            {
+                new DiscreteObjectKeyFrame(frame1, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame2, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame3, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame4, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame5, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame6, KeyTime.Paced),
+                new DiscreteObjectKeyFrame(frame7, KeyTime.Paced),
+            },
+            RepeatBehavior = RepeatBehavior.Forever,
+            Duration = new Duration(new TimeSpan(0, 0, 0, 0, 500))
+        });
+    }
+
+    protected override void OnRender(DrawingContext drawingContext)
+    {
+        base.OnRender(drawingContext);
+        if (Path is null)
+            return;
+
+        renderPath = new PathGeometry()
+        {
+            FillRule = FillRule.Nonzero,
+            Figures = (PathFigureCollection?)converter.ConvertFromString(Path.ToSvgPathData()),
+        };
+        drawingContext.DrawGeometry(null, whitePen, renderPath);
+        drawingContext.DrawGeometry(null, blackDashedPen, renderPath);
+    }
+
+    private static void OnZoomboxScaleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+    {
+        var self = (SelectionOverlay)obj;
+        double newScale = (double)args.NewValue;
+        self.whitePen.Thickness = 1.0 / newScale;
+        self.blackDashedPen.Thickness = 1.0 / newScale;
+    }
+}

+ 9 - 0
src/PixiEditorPrototype/Models/DocumentUpdater.cs

@@ -5,6 +5,7 @@ using System.Windows.Media.Imaging;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -66,9 +67,17 @@ internal class DocumentUpdater
             case LayerLockTransparency_ChangeInfo info:
                 ProcessLayerLockTransparency(info);
                 break;
+            case Selection_ChangeInfo info:
+                ProcessSelection(info);
+                break;
         }
     }
 
+    private void ProcessSelection(Selection_ChangeInfo info)
+    {
+        doc.RaisePropertyChanged(nameof(doc.SelectionPath));
+    }
+
     private void ProcessLayerLockTransparency(LayerLockTransparency_ChangeInfo info)
     {
         var layer = (LayerViewModel)helper.StructureHelper.FindOrThrow(info.GuidValue);

+ 0 - 5
src/PixiEditorPrototype/Themes/Generic.xaml

@@ -1,5 +0,0 @@
-<ResourceDictionary
-    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    xmlns:local="clr-namespace:PixiEditorPrototype">
-</ResourceDictionary>

+ 2 - 0
src/PixiEditorPrototype/UserControls/Viewport/Viewport.xaml

@@ -7,6 +7,7 @@
              xmlns:local="clr-namespace:PixiEditorPrototype.UserControls.Viewport"
              xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
              xmlns:to="clr-namespace:PixiEditorPrototype.CustomControls.TransformOverlay"
+             xmlns:cust="clr-namespace:PixiEditorPrototype.CustomControls"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
              xmlns:conv="clr-namespace:PixiEditorPrototype.Converters"
              mc:Ignorable="d"
@@ -43,6 +44,7 @@
                             </i:EventTrigger>
                         </i:Interaction.Triggers>
                     </Image>
+                    <cust:SelectionOverlay Path="{Binding Document.SelectionPath}" ZoomboxScale="{Binding Zoombox.Scale}"/>
                     <to:TransformOverlay HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                                             Visibility="{Binding Document.TransformViewModel.TransformActive, Converter={StaticResource BoolToVisibilityConverter}}"
                                             Corners="{Binding Document.TransformViewModel.Corners, Mode=TwoWay}"

+ 3 - 2
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -70,6 +70,7 @@ internal class DocumentViewModel : INotifyPropertyChanged
 
     public int Width => Helpers.Tracker.Document.Size.X;
     public int Height => Helpers.Tracker.Document.Size.Y;
+    public SKPath SelectionPath => Helpers.Tracker.Document.ReadOnlySelection.ReadOnlySelectionPath;
     public Guid GuidValue { get; } = Guid.NewGuid();
 
     public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
@@ -181,8 +182,8 @@ internal class DocumentViewModel : INotifyPropertyChanged
     bool startedSelection = false;
     public void StartUpdateSelection(Vector2i pos, Vector2i size)
     {
-        if (!startedSelection)
-            Helpers.ActionAccumulator.AddActions(new ClearSelection_Action());
+        //if (!startedSelection)
+        //   Helpers.ActionAccumulator.AddActions(new ClearSelection_Action());
         startedSelection = true;
         updateableChangeActive = true;
         Helpers.ActionAccumulator.AddActions(new SelectRectangle_Action(pos, size));