Ver Fonte

avalonia: Implemented pan mode for image editor; fixed some bugs with DimensionObject in ImageEditor

Kenric Nugteren há 1 semana atrás
pai
commit
b467b462bf

+ 4 - 2
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml

@@ -14,6 +14,7 @@
 					Value="Transparent"/>
 			<Setter Property="ClipToBounds"
 					Value="False"/>
+			<Setter Property="CornerRadius" Value="3"/>
 		</Style>
 		<Style Selector="Button:flyout-open /template/ ContentPresenter">
 			<Setter Property="BoxShadow"
@@ -71,7 +72,7 @@
 			</Grid.RowDefinitions>
 			<Canvas Name="OuterCanvas" Grid.Row="1" Background="White" PointerWheelChanged="OuterCanvas_PointerWheelChanged">
 				<Canvas.GestureRecognizers>
-					<components:PanAndZoomGestureRecognizer/>
+					<components:PanAndZoomGestureRecognizer Name="GestureRecognizer"/>
 				</Canvas.GestureRecognizers>
 				<Canvas.Styles>
 					<Style Selector="Thumb">
@@ -79,7 +80,8 @@
 							<ControlTemplate>
 								<Border Background="LightGray"
 										BorderBrush="Black"
-										BorderThickness="1"/>
+										BorderThickness="{TemplateBinding BorderThickness}"
+										CornerRadius="3"/>
 							</ControlTemplate>
 						</Setter>
 					</Style>

+ 36 - 3
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml.cs

@@ -13,11 +13,13 @@ using InABox.Avalonia.Converters;
 using System.Collections.ObjectModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using InABox.Avalonia.Dialogs;
+using Avalonia.Data.Converters;
 
 namespace InABox.Avalonia.Components;
 
 public enum ImageEditingMode
 {
+    Select,
     Polyline,
     Rectangle,
     Ellipse,
@@ -90,6 +92,11 @@ public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBru
 
 // TODO: Make it so we don't re-render everything everytime 'Objects' changes.
 
+public class ImageEditorState(double scaleFactor)
+{
+    public double ScaleFactor { get; } = scaleFactor;
+}
+
 public partial class ImageEditor : UserControl
 {
     public static readonly StyledProperty<IImage?> SourceProperty =
@@ -111,7 +118,7 @@ public partial class ImageEditor : UserControl
         AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);
 
     public static readonly StyledProperty<ImageEditingMode> ModeProperty =
-        AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);
+        AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Select);
 
     public static readonly StyledProperty<bool> ShowButtonsProperty =
         AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
@@ -202,12 +209,23 @@ public partial class ImageEditor : UserControl
 
     private Stack<IImageEditorObject> RedoStack = new();
 
-    private double ScaleFactor = 1.0;
+    private double _scaleFactor = 1.0;
+    private double ScaleFactor
+    {
+        get => _scaleFactor;
+        set
+        {
+            _scaleFactor = value;
+            (CurrentObject as IImageEditorStateObject)?.SetState(State);
+        }
+    }
     private double _originalScaleFactor = 1.0;
 
     // Center of the image.
     private Point ImageCenter = new();
 
+    private ImageEditorState State => new(ScaleFactor);
+
     #endregion
 
     static ImageEditor()
@@ -394,6 +412,7 @@ public partial class ImageEditor : UserControl
             button.Active = button.Mode == mode;
         }
         Mode = mode;
+        GestureRecognizer.IsEnabled = Mode == ImageEditingMode.Select;
         // ShapeButton.Content = CreateModeButtonContent(mode);
         SecondaryColour.IsVisible = HasSecondaryColour();
         LineThicknessButton.IsVisible = HasLineThickness();
@@ -419,6 +438,7 @@ public partial class ImageEditor : UserControl
 
     private void AddModeButtons()
     {
+        AddModeButton(ImageEditingMode.Select);
         AddModeButton(ImageEditingMode.Polyline);
         AddModeButton(ImageEditingMode.Rectangle);
         AddModeButton(ImageEditingMode.Ellipse);
@@ -435,6 +455,14 @@ public partial class ImageEditor : UserControl
     {
         switch (mode)
         {
+            case ImageEditingMode.Select:
+                var selectImage = new Image
+                {
+                    Source = Images.move,
+                    Width = 25,
+                    Height = 25
+                };
+                return selectImage;
             case ImageEditingMode.Polyline:
                 var canvas = new Canvas();
                 {
@@ -674,6 +702,8 @@ public partial class ImageEditor : UserControl
 
         CurrentObject = null;
 
+        var state = new ImageEditorState(1);
+
         foreach (var obj in Objects)
         {
             var control = obj.GetControl();
@@ -875,7 +905,10 @@ public partial class ImageEditor : UserControl
                 }
                 dimension.Complete = true;
 
-                Navigation.Popup<TextDialogViewModel, string?>(x => { }).ContinueWith(task =>
+                Navigation.Popup<TextDialogViewModel, string?>(x =>
+                {
+                    x.Title = "Enter Dimension:";
+                }).ContinueWith(task =>
                 {
                     dimension.Text = task.Result ?? "";
                     dimension.Update();

+ 63 - 7
InABox.Avalonia/Components/ImageEditor/Objects/DimensionObject.cs

@@ -13,7 +13,7 @@ using System.Xml;
 
 namespace InABox.Avalonia.Components.ImageEditing;
 
-internal class DimensionObject : IImageEditorObject
+internal class DimensionObject : IImageEditorStateObject
 {
     public IBrush? PrimaryBrush { get; set; }
 
@@ -35,6 +35,8 @@ internal class DimensionObject : IImageEditorObject
 
     private bool _active = true;
 
+    private ImageEditorState _state = new(1.0);
+
     public Control GetControl() => Control;
 
     private Vector GetMainVec()
@@ -62,6 +64,50 @@ internal class DimensionObject : IImageEditorObject
         return new Vector(mainVec.Y, -mainVec.X);
     }
 
+    public void SetState(ImageEditorState state)
+    {
+        _state = state;
+
+        var p1 = new Vector(Point1.X, Point1.Y);
+        var p2 = new Vector(Point2.X, Point2.Y);
+
+        var mainVec = p2 - p1;
+        var length = mainVec.Length;
+        if(length == 0)
+        {
+            mainVec = new Vector(0, 0);
+        }
+        else
+        {
+            mainVec /= length;
+        }
+
+        var perpVec = new Vector(mainVec.Y, -mainVec.X);
+
+        var arrowLength = LineThickness * 2;
+        var offset = perpVec * Offset;
+
+        var thumbSize = 30;
+
+        if(Thumb is not null)
+        {
+            Thumb.RenderTransform = new TransformGroup
+            {
+                Children = [
+                    new RotateTransform(Math.Atan2(mainVec.Y, mainVec.X) * 180 / Math.PI),
+                    new ScaleTransform(1 / _state.ScaleFactor, 1 / _state.ScaleFactor)
+                    ]
+            };
+            Thumb.BorderThickness = new(1);
+            Thumb.Width = thumbSize;
+            Thumb.Height = thumbSize;
+
+            var thumbPos = p1 + offset + mainVec * length / 2;
+            Canvas.SetLeft(Thumb, thumbPos.X - thumbSize / 2);
+            Canvas.SetTop(Thumb, thumbPos.Y - thumbSize / 2);
+        }
+    }
+
     public void Update()
     {
         Control.Children.RemoveAll(Control.Children.Where(x => !(x is Thumb)));
@@ -81,6 +127,7 @@ internal class DimensionObject : IImageEditorObject
         {
             mainVec /= length;
         }
+
         var perpVec = new Vector(mainVec.Y, -mainVec.X);
 
         var arrowLength = LineThickness * 2;
@@ -166,11 +213,11 @@ internal class DimensionObject : IImageEditorObject
 
         Control.Children.Add(text);
 
+        var thumbSize = 30;
+
         if(Thumb is null && _active && length > 0)
         {
             Thumb = new();
-            Thumb.Width = 10;
-            Thumb.Height = 10;
             Thumb.Cursor = new(StandardCursorType.SizeAll);
             Thumb.DragDelta += Thumb_DragDelta;
             Thumb.ZIndex = 1;
@@ -180,11 +227,20 @@ internal class DimensionObject : IImageEditorObject
 
         if(Thumb is not null)
         {
-            Thumb.RenderTransform = new RotateTransform(Math.Atan2(mainVec.Y, mainVec.X) * 180 / Math.PI);
+            Thumb.RenderTransform = new TransformGroup
+            {
+                Children = [
+                    new RotateTransform(Math.Atan2(mainVec.Y, mainVec.X) * 180 / Math.PI),
+                    new ScaleTransform(1 / _state.ScaleFactor, 1 / _state.ScaleFactor)
+                    ]
+            };
+            Thumb.BorderThickness = new(1);
+            Thumb.Width = thumbSize;
+            Thumb.Height = thumbSize;
 
             var thumbPos = p1 + offset + mainVec * length / 2;
-            Canvas.SetLeft(Thumb, thumbPos.X - 5);
-            Canvas.SetTop(Thumb, thumbPos.Y - 5);
+            Canvas.SetLeft(Thumb, thumbPos.X - thumbSize / 2);
+            Canvas.SetTop(Thumb, thumbPos.Y - thumbSize / 2);
         }
     }
 
@@ -200,7 +256,7 @@ internal class DimensionObject : IImageEditorObject
     private void Thumb_DragDelta(object? sender, VectorEventArgs e)
     {
         var main = GetMainVec();
-        var v = Vector.Dot(Rotate(e.Vector, Math.Atan2(main.Y, main.X)), GetPerpVec());
+        var v = Vector.Dot(Rotate(e.Vector, Math.Atan2(main.Y, main.X)), GetPerpVec()) / _state.ScaleFactor;
         Offset += v;
         Update();
     }

+ 5 - 0
InABox.Avalonia/Components/ImageEditor/Objects/IImageEditorObject.cs

@@ -19,3 +19,8 @@ internal interface IImageEditorObject
 
     void Update();
 }
+
+internal interface IImageEditorStateObject : IImageEditorObject
+{
+    void SetState(ImageEditorState state);
+}

+ 25 - 4
InABox.Avalonia/Components/ImageEditor/PanAndZoomGestureRecognizer.cs

@@ -39,6 +39,24 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
         RoutedEvent.Register<PanAndZoomEndedEventArgs>(
             "PanAndZoomEnded", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer));
 
+    public static readonly StyledProperty<bool> IsEnabledProperty =
+        AvaloniaProperty.Register<PanAndZoomGestureRecognizer, bool>(nameof(IsEnabled), defaultValue: true);
+
+    public static readonly StyledProperty<bool> SingleTouchPanningProperty =
+        AvaloniaProperty.Register<PanAndZoomGestureRecognizer, bool>(nameof(SingleTouchPanning), defaultValue: true);
+
+    public bool IsEnabled
+    {
+        get => GetValue(IsEnabledProperty);
+        set => SetValue(IsEnabledProperty, value);
+    }
+
+    public bool SingleTouchPanning
+    {
+        get => GetValue(SingleTouchPanningProperty);
+        set => SetValue(SingleTouchPanningProperty, value);
+    }
+
     private float _initialDistance;
     private IPointer? _firstContact;
     private Point _firstPoint;
@@ -54,6 +72,7 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
 
     protected override void PointerMoved(PointerEventArgs e)
     {
+        if (!IsEnabled) return;
         if (Target is Visual visual)
         {
             if (_firstContact == e.Pointer)
@@ -84,7 +103,7 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
                 e.Handled = pinchEventArgs.Handled;
                 e.PreventGestureRecognition();
             }
-            else if(_firstContact != null)
+            else if(_firstContact != null && SingleTouchPanning)
             {
                 var pinchEventArgs = new PanAndZoomEventArgs(1, _firstPoint, _firstPoint - _initialFirstPoint);
                 _initialFirstPoint = _firstPoint;
@@ -97,6 +116,7 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
 
     protected override void PointerPressed(PointerPressedEventArgs e)
     {
+        if (!IsEnabled) return;
         if (Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
         {
             if (_firstContact == null)
@@ -124,7 +144,7 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
                 Capture(_secondContact);
                 e.PreventGestureRecognition();
             }
-            else if(_firstContact != null)
+            else if(_firstContact != null && SingleTouchPanning)
             {
                 _initialFirstPoint = _firstPoint;
                 Capture(_firstContact);
@@ -167,7 +187,8 @@ public class PanAndZoomGestureRecognizer : GestureRecognizer
 
     private static float GetDistance(Point a, Point b)
     {
-        var length = b - a;
-        return (float)new Vector(length.X, length.Y).Length;
+        var distX = a.X - b.X;
+        var distY = a.Y - b.Y;
+        return (float)Math.Sqrt(distX * distX + distY * distY);
     }
 }

+ 1 - 0
InABox.Avalonia/Images/Images.cs

@@ -21,5 +21,6 @@ public static class Images
         return result;
     }
     
+    public static SvgImage? move => LoadSVG("/Images/move.svg");
     public static SvgImage? search => LoadSVG("/Images/search.svg");
 }

+ 59 - 0
InABox.Avalonia/Images/move.svg

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 492.009 492.009" style="enable-background:new 0 0 492.009 492.009;" xml:space="preserve">
+<g>
+	<g>
+		<path d="M314.343,62.977L255.399,4.033c-2.672-2.672-6.236-4.04-9.92-4.032c-3.752-0.036-7.396,1.36-10.068,4.032l-57.728,57.728    c-5.408,5.408-5.408,14.2,0,19.604l7.444,7.444c5.22,5.22,14.332,5.22,19.556,0l22.1-22.148v81.388    c0,0.248,0.144,0.452,0.188,0.684c0.6,7.092,6.548,12.704,13.8,12.704h10.52c7.644,0,13.928-6.208,13.928-13.852v-9.088    c0-0.04,0-0.068,0-0.1V67.869l22.108,22.152c5.408,5.408,14.18,5.408,19.584,0l7.432-7.436    C319.751,77.173,319.751,68.377,314.343,62.977z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M314.335,409.437l-7.44-7.456c-5.22-5.228-14.336-5.228-19.564,0l-22.108,22.152v-70.216c0-0.04,0-0.064,0-0.1v-9.088    c0-7.648-6.288-14.16-13.924-14.16h-10.528c-7.244,0-13.192,5.756-13.796,12.856c-0.044,0.236-0.188,0.596-0.188,0.84v81.084    l-22.1-22.148c-5.224-5.224-14.356-5.224-19.58,0l-7.44,7.444c-5.4,5.404-5.392,14.2,0.016,19.608l57.732,57.724    c2.604,2.612,6.08,4.032,9.668,4.032h0.52c3.716,0,7.184-1.416,9.792-4.032l58.94-58.94    C319.743,423.633,319.743,414.841,314.335,409.437z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M147.251,226.781l-1.184,0h-7.948c-0.028,0-0.056,0-0.088,0h-69.88l22.152-22.032c2.612-2.608,4.048-6.032,4.048-9.74    c0-3.712-1.436-7.164-4.048-9.768l-7.444-7.428c-5.408-5.408-14.204-5.4-19.604,0.008l-58.944,58.94    c-2.672,2.668-4.1,6.248-4.028,9.92c-0.076,3.82,1.356,7.396,4.028,10.068l57.728,57.732c2.704,2.704,6.252,4.056,9.804,4.056    s7.1-1.352,9.804-4.056l7.44-7.44c2.612-2.608,4.052-6.092,4.052-9.8c0-3.712-1.436-7.232-4.052-9.836l-22.144-22.184h80.728    c0.244,0,0.644-0.06,0.876-0.104c7.096-0.6,12.892-6.468,12.892-13.716v-10.536C161.439,233.229,154.895,226.781,147.251,226.781z    "/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M487.695,236.765l-58.944-58.936c-5.404-5.408-14.2-5.408-19.604,0l-7.436,7.444c-2.612,2.604-4.052,6.088-4.052,9.796    c0,3.712,1.436,7.072,4.052,9.68l22.148,22.032h-70.328c-0.036,0-0.064,0-0.096,0h-9.084c-7.644,0-13.78,6.444-13.78,14.084    v10.536c0,7.248,5.564,13.108,12.664,13.712c0.236,0.048,0.408,0.108,0.648,0.108h81.188l-22.156,22.18    c-2.608,2.604-4.048,6.116-4.048,9.816c0,3.716,1.436,7.208,4.048,9.816l7.448,7.444c2.7,2.704,6.248,4.06,9.8,4.06    s7.096-1.352,9.8-4.056l57.736-57.732c2.664-2.664,4.092-6.244,4.028-9.92C491.787,243.009,490.359,239.429,487.695,236.765z"/>
+	</g>
+</g>
+<g>
+	<g>
+		<path d="M246.011,207.541c-21.204,0-38.456,17.252-38.456,38.46c0,21.204,17.252,38.46,38.456,38.46    c21.204,0,38.46-17.256,38.46-38.46C284.471,224.793,267.215,207.541,246.011,207.541z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 4 - 0
InABox.Avalonia/InABox.Avalonia.csproj

@@ -12,6 +12,7 @@
 
     <ItemGroup>
       <None Remove="Images\cross.svg" />
+      <None Remove="Images\move.svg" />
       <None Remove="Images\refresh.svg" />
       <None Remove="Images\search.svg" />
       <None Remove="Images\tick.svg" />
@@ -53,6 +54,9 @@
       <AvaloniaResource Include="Images\cross.svg">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </AvaloniaResource>
+      <AvaloniaResource Include="Images\move.svg">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </AvaloniaResource>
       <AvaloniaResource Include="Images\refresh.svg">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </AvaloniaResource>