Browse Source

Symmetry overlay is working

Krzysztof Krysiński 1 year ago
parent
commit
11ac4f9030

+ 17 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/ZoomModeToHitTestVisibleConverter.cs

@@ -0,0 +1,17 @@
+using System.Globalization;
+using System.Windows;
+using Avalonia;
+using PixiEditor.AvaloniaUI.Helpers.Converters;
+using PixiEditor.Zoombox;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class ZoomModeToHitTestVisibleConverter : SingleInstanceConverter<ZoomModeToHitTestVisibleConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is not ZoomboxMode zoomboxMode)
+            return AvaloniaProperty.UnsetValue;
+        return zoomboxMode == ZoomboxMode.Normal;
+    }
+}

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ChangeExecutionController.cs

@@ -5,6 +5,7 @@ using PixiEditor.AvaloniaUI.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.Models.Tools;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/Public/DocumentEventsModule.cs

@@ -2,6 +2,7 @@
 using PixiEditor.AvaloniaUI.Models.Events;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Position;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/SymmetryExecutor.cs

@@ -1,5 +1,6 @@
 using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.Models.Tools;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Enums;
 

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -6,6 +6,7 @@ using PixiEditor.AvaloniaUI.Exceptions;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.Models.Tools;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
 

+ 1 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentManagerViewModel.cs

@@ -9,6 +9,7 @@ using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.AvaloniaUI.Views;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;

+ 3 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -21,6 +21,7 @@ using PixiEditor.AvaloniaUI.Models.Position;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Document.TransformOverlays;
+using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
@@ -508,7 +509,8 @@ internal partial class DocumentViewModel : ObservableObject, IDocument
 
     public void SetVerticalSymmetryAxisEnabled(bool infoState)
     {
-        throw new NotImplementedException();
+        verticalSymmetryAxisEnabled = infoState;
+        OnPropertyChanged(nameof(VerticalSymmetryAxisEnabledBindable));
     }
 
     public void SetVerticalSymmetryAxisX(double verticalSymmetryAxisX)

+ 6 - 5
src/PixiEditor.AvaloniaUI/Views/Main/Viewport.axaml

@@ -17,6 +17,7 @@
     xmlns:subviews="clr-namespace:PixiEditor.AvaloniaUI.ViewModels.Document"
     xmlns:brushShapeOverlay="clr-namespace:PixiEditor.AvaloniaUI.Views.Overlays.BrushShapeOverlay"
     xmlns:viewModels="clr-namespace:PixiEditor.AvaloniaUI.ViewModels"
+    xmlns:symmetryOverlay="clr-namespace:PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay"
     mc:Ignorable="d"
     x:Name="vpUc"
     d:DesignHeight="450"
@@ -262,7 +263,7 @@
                     </Image>
                     <Grid ZIndex="5">
                         <!--TODO: Implement overlays-->
-                        <!--<symOverlay:SymmetryOverlay
+                        <symmetryOverlay:SymmetryOverlay
                             Focusable="False"
                             IsHitTestVisible="{Binding ZoomMode, Converter={converters:ZoomModeToHitTestVisibleConverter}}"
                             ZoomboxScale="{Binding Zoombox.Scale}"
@@ -270,11 +271,11 @@
                             VerticalAxisVisible="{Binding Document.VerticalSymmetryAxisEnabledBindable}"
                             HorizontalAxisY="{Binding Document.HorizontalSymmetryAxisYBindable, Mode=OneWay}"
                             VerticalAxisX="{Binding Document.VerticalSymmetryAxisXBindable, Mode=OneWay}"
-                            DragCommand="{cmds:Command PixiEditor.Document.DragSymmetry, UseProvided=True}"
-                            DragEndCommand="{cmds:Command PixiEditor.Document.EndDragSymmetry, UseProvided=True}"
-                            DragStartCommand="{cmds:Command PixiEditor.Document.StartDragSymmetry, UseProvided=True}"
+                            DragCommand="{xaml:Command PixiEditor.Document.DragSymmetry, UseProvided=True}"
+                            DragEndCommand="{xaml:Command PixiEditor.Document.EndDragSymmetry, UseProvided=True}"
+                            DragStartCommand="{xaml:Command PixiEditor.Document.StartDragSymmetry, UseProvided=True}"
                             FlowDirection="LeftToRight" />
-                        <overlays:SelectionOverlay
+                        <!--<overlays:SelectionOverlay
                             Focusable="False"
                             ShowFill="{Binding ToolsSubViewModel.ActiveTool, Source={vm:MainVM}, Converter={converters:IsSelectionToolConverter}}"
                             Path="{Binding Document.SelectionPathBindable}"

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Position/SymmetryAxisDragInfo.cs → src/PixiEditor.AvaloniaUI/Views/Overlays/SymmetryOverlay/SymmetryAxisDragInfo.cs

@@ -1,5 +1,5 @@
 using PixiEditor.ChangeableDocument.Enums;
 
-namespace PixiEditor.AvaloniaUI.Models.Position;
+namespace PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
 #nullable enable
 internal record class SymmetryAxisDragInfo(SymmetryAxisDirection Direction, double NewPosition);

+ 443 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -0,0 +1,443 @@
+using System.Globalization;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Hardware.Info;
+using PixiEditor.AvaloniaUI.Models.Controllers.InputDevice;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
+#nullable enable
+internal class SymmetryOverlay : Control
+{
+    public static readonly StyledProperty<double> HorizontalAxisYProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, double>(nameof(HorizontalAxisY), defaultValue: 0.0);
+
+    public double HorizontalAxisY
+    {
+        get => GetValue(HorizontalAxisYProperty);
+        set => SetValue(HorizontalAxisYProperty, value);
+    }
+
+    public static readonly StyledProperty<double> VerticalAxisXProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, double>(nameof(VerticalAxisX), defaultValue: 0.0);
+
+    public double VerticalAxisX
+    {
+        get => GetValue(VerticalAxisXProperty);
+        set => SetValue(VerticalAxisXProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> HorizontalAxisVisibleProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, bool>(nameof(HorizontalAxisVisible), defaultValue: false);
+
+    public bool HorizontalAxisVisible
+    {
+        get => GetValue(HorizontalAxisVisibleProperty);
+        set => SetValue(HorizontalAxisVisibleProperty, value);
+    }
+
+    public static readonly StyledProperty<bool> VerticalAxisVisibleProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, bool>(nameof(VerticalAxisVisible), defaultValue: false);
+
+    public bool VerticalAxisVisible
+    {
+        get => GetValue(VerticalAxisVisibleProperty);
+        set => SetValue(VerticalAxisVisibleProperty, value);
+    }
+
+    public static readonly StyledProperty<double> ZoomboxScaleProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, double>(nameof(ZoomboxScale), defaultValue: 1.0);
+
+    public double ZoomboxScale
+    {
+        get => GetValue(ZoomboxScaleProperty);
+        set => SetValue(ZoomboxScaleProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand?> DragCommandProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, ICommand?>(nameof(DragCommand));
+
+    public ICommand? DragCommand
+    {
+        get => GetValue(DragCommandProperty);
+        set => SetValue(DragCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand?> DragEndCommandProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, ICommand?>(nameof(DragEndCommand));
+
+    public ICommand? DragEndCommand
+    {
+        get => GetValue(DragEndCommandProperty);
+        set => SetValue(DragEndCommandProperty, value);
+    }
+
+    public static readonly StyledProperty<ICommand?> DragStartCommandProperty =
+        AvaloniaProperty.Register<SymmetryOverlay, ICommand?>(nameof(DragStartCommand));
+
+    public ICommand? DragStartCommand
+    {
+        get => GetValue(DragStartCommandProperty);
+        set => SetValue(DragStartCommandProperty, value);
+    }
+
+    static SymmetryOverlay()
+    {
+        AffectsRender<SymmetryOverlay>(HorizontalAxisVisibleProperty);
+        AffectsRender<SymmetryOverlay>(VerticalAxisVisibleProperty);
+        AffectsRender<SymmetryOverlay>(ZoomboxScaleProperty);
+
+        HorizontalAxisYProperty.Changed.Subscribe(OnPositionUpdate);
+        VerticalAxisXProperty.Changed.Subscribe(OnPositionUpdate);
+    }
+
+    private const double HandleSize = 12;
+    private PathGeometry handleGeometry = new()
+    {
+        FillRule = FillRule.NonZero,
+        Figures = PathFigures.Parse($"M -1.1146 -0.6603 c -0.1215 -0.1215 -0.3187 -0.1215 -0.4401 0 l -0.4401 0.4401 c -0.1215 0.1215 -0.1215 0.3187 0 0.4401 l 0.4401 0.4401 c 0.1215 0.1215 0.3187 0.1215 0.4401 0 l 0.4401 -0.4401 c 0.1215 -0.1215 0.1215 -0.3187 0 -0.4401 l -0.4401 -0.4401 Z M -0.5834 0.0012 l 0.5833 -0.0013"),
+    };
+
+    private const double DashWidth = 10.0;
+    const int RulerOffset = -35;
+    const int RulerWidth = 4;
+
+    private Brush handleFill = new SolidColorBrush(Brushes.Transparent.Color, 0);
+    private Pen rulerPen = new(Brushes.White, 1.0);
+    private Pen borderPen = new(new SolidColorBrush(Color.FromRgb(200, 200, 200)), 1.0);
+    private Pen checkerBlack = new(new SolidColorBrush(Color.FromRgb(170, 170, 170)), 1.0) { DashStyle = new DashStyle(new[] { DashWidth, DashWidth }, 0) };
+    private Pen checkerWhite = new(new SolidColorBrush(Color.FromRgb(100, 100, 100)), 1.0) { DashStyle = new DashStyle(new[] { DashWidth, DashWidth }, DashWidth) };
+
+    private double PenThickness => 1.0 / ZoomboxScale;
+
+    private double horizontalAxisY;
+    private double verticalAxisX;
+    private Point pointerPosition;
+
+    private MouseUpdateController mouseUpdateController;
+
+    public SymmetryOverlay()
+    {
+        Loaded += OnLoaded;
+    }
+
+    private void OnLoaded(object sender, RoutedEventArgs e)
+    {
+        mouseUpdateController = new MouseUpdateController(this, MouseMoved);
+        PointerEntered += OnPointerEntered;
+    }
+
+    private void OnPointerEntered(object? sender, PointerEventArgs e)
+    {
+        pointerPosition = e.GetPosition(this);
+    }
+
+    public override void Render(DrawingContext drawingContext)
+    {
+        base.Render(drawingContext);
+        if (!HorizontalAxisVisible && !VerticalAxisVisible)
+            return;
+
+        borderPen.Thickness = 3 * PenThickness;
+        checkerBlack.Thickness = PenThickness;
+        checkerWhite.Thickness = PenThickness;
+        rulerPen.Thickness = PenThickness;
+
+        handleGeometry.Transform = new ScaleTransform(HandleSize / ZoomboxScale, HandleSize / ZoomboxScale);
+
+        if (HorizontalAxisVisible)
+        {
+            if (capturedDirection == SymmetryAxisDirection.Horizontal || hoveredDirection == SymmetryAxisDirection.Horizontal)
+            {
+                if (horizontalAxisY != 0)
+                {
+                    DrawHorizontalRuler(drawingContext, false);
+                }
+
+                if (horizontalAxisY != (int)Bounds.Height)
+                {
+                    DrawHorizontalRuler(drawingContext, true);
+                }
+            }
+
+            var transformState = drawingContext.PushTransform(new TranslateTransform(0, horizontalAxisY).Value);
+            drawingContext.DrawGeometry(handleFill, borderPen, handleGeometry);
+            var rotateState = drawingContext.PushTransform(new RotateTransform(180, Bounds.Width / 2, 0).Value);
+            drawingContext.DrawGeometry(handleFill, borderPen, handleGeometry);
+
+            rotateState.Dispose();
+            transformState.Dispose();
+
+            drawingContext.DrawLine(checkerBlack, new(0, horizontalAxisY), new(Bounds.Width, horizontalAxisY));
+            drawingContext.DrawLine(checkerWhite, new(0, horizontalAxisY), new(Bounds.Width, horizontalAxisY));
+        }
+        if (VerticalAxisVisible)
+        {
+            if (capturedDirection == SymmetryAxisDirection.Vertical || hoveredDirection == SymmetryAxisDirection.Vertical)
+            {
+                if (verticalAxisX != 0)
+                {
+                    DrawVerticalRuler(drawingContext, false);
+                }
+
+                if (verticalAxisX != (int)Bounds.Width)
+                {
+                    DrawVerticalRuler(drawingContext, true);
+                }
+            }
+
+
+
+            var rotation = drawingContext.PushTransform(new RotateTransform(90).Value);
+            var translation = drawingContext.PushTransform(new TranslateTransform(0, -verticalAxisX).Value);
+            drawingContext.DrawGeometry(handleFill, borderPen, handleGeometry);
+            var rotation1 = drawingContext.PushTransform(new RotateTransform(180, Bounds.Height / 2, 0).Value);
+            drawingContext.DrawGeometry(handleFill, borderPen, handleGeometry);
+
+            rotation1.Dispose();
+            translation.Dispose();
+            rotation.Dispose();
+
+            drawingContext.DrawLine(checkerBlack, new(verticalAxisX, 0), new(verticalAxisX, Bounds.Height));
+            drawingContext.DrawLine(checkerWhite, new(verticalAxisX, 0), new(verticalAxisX, Bounds.Height));
+        }
+    }
+
+    private void DrawHorizontalRuler(DrawingContext drawingContext, bool upper)
+    {
+        double start = upper ? Bounds.Height : 0;
+        bool drawRight = pointerPosition.X > Bounds.Width / 2;
+        double xOffset = drawRight ? Bounds.Width - RulerOffset * PenThickness * 2 : 0;
+
+        drawingContext.DrawLine(rulerPen, new Point(RulerOffset * PenThickness + xOffset, start), new Point(RulerOffset * PenThickness + xOffset, horizontalAxisY));
+        drawingContext.DrawLine(rulerPen, new Point((RulerOffset - RulerWidth) * PenThickness + xOffset, start), new Point((RulerOffset + RulerWidth) * PenThickness + xOffset, start));
+        drawingContext.DrawLine(rulerPen, new Point((RulerOffset - RulerWidth) * PenThickness + xOffset, horizontalAxisY), new Point((RulerOffset + RulerWidth) * PenThickness + xOffset, horizontalAxisY));
+
+        string text = upper ? $"{start - horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({(start - horizontalAxisY) / Bounds.Height * 100:F1}%)‎" : $"{horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({horizontalAxisY / Bounds.Height * 100:F1}%)‎";
+
+        var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
+            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomboxScale, Brushes.White);
+
+        if (Bounds.Height < formattedText.Height * 2.5 || horizontalAxisY == (int)Bounds.Height && upper || horizontalAxisY == 0 && !upper)
+        {
+            return;
+        }
+
+        formattedText.TextAlignment = drawRight ? TextAlignment.Left : TextAlignment.Right;
+
+        double textY = horizontalAxisY / 2.0 - formattedText.Height / 2;
+
+        if (upper)
+        {
+            textY += Bounds.Height / 2;
+        }
+
+        drawingContext.DrawText(formattedText, new Point(RulerOffset * PenThickness - (drawRight ? -1 : 1) + xOffset, textY));
+    }
+
+    private void DrawVerticalRuler(DrawingContext drawingContext, bool right)
+    {
+        double start = right ? Bounds.Width : 0;
+        bool drawBottom = pointerPosition.Y > Bounds.Height / 2;
+        double yOffset = drawBottom ? Bounds.Height - RulerOffset * PenThickness * 2 : 0;
+
+        drawingContext.DrawLine(rulerPen, new Point(start, RulerOffset * PenThickness + yOffset), new Point(verticalAxisX, RulerOffset * PenThickness + yOffset));
+        drawingContext.DrawLine(rulerPen, new Point(start, (RulerOffset - RulerWidth) * PenThickness + yOffset), new Point(start, (RulerOffset + RulerWidth) * PenThickness + yOffset));
+        drawingContext.DrawLine(rulerPen, new Point(verticalAxisX, (RulerOffset - RulerWidth) * PenThickness + yOffset), new Point(verticalAxisX, (RulerOffset + RulerWidth) * PenThickness + yOffset));
+
+        string text = right ? $"{start - verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({(start - verticalAxisX) / Bounds.Width * 100:F1}%)‎" : $"{verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({verticalAxisX / Bounds.Width * 100:F1}%)‎";
+
+        var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
+            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomboxScale, Brushes.White);
+
+        if (Bounds.Width < formattedText.Width * 2.5 || verticalAxisX == (int)Bounds.Width && right || verticalAxisX == 0 && !right)
+        {
+            return;
+        }
+
+        formattedText.TextAlignment = TextAlignment.Center;
+
+        double textX = verticalAxisX / 2.0;
+
+        if (right)
+        {
+            textX += Bounds.Width / 2;
+        }
+
+        drawingContext.DrawText(formattedText, new Point(textX, RulerOffset * PenThickness - (drawBottom ? -0.7 : 0.3 + formattedText.Height) + yOffset));
+    }
+
+    //TODO: I didn't find HitTestCore in Avalonia
+    /*protected override HitTestResult? HitTestCore(PointHitTestParameters hitTestParameters)
+    {
+        // prevent the line from blocking mouse input
+        var point = hitTestParameters.HitPoint;
+        if (point.X > 0 && point.Y > 0 && point.X < Bounds.Width && point.Y < Bounds.Height)
+            return null;
+
+        return new PointHitTestResult(this, hitTestParameters.HitPoint);
+    }*/
+
+    private SymmetryAxisDirection? IsTouchingHandle(VecD position)
+    {
+        double radius = HandleSize * 4 / ZoomboxScale / 2;
+        VecD left = new(-radius, horizontalAxisY);
+        VecD right = new(Bounds.Width + radius, horizontalAxisY);
+        VecD up = new(verticalAxisX, -radius);
+        VecD down = new(verticalAxisX, Bounds.Height + radius);
+
+        if (HorizontalAxisVisible && (Math.Abs((left - position).LongestAxis) < radius || Math.Abs((right - position).LongestAxis) < radius))
+            return SymmetryAxisDirection.Horizontal;
+        if (VerticalAxisVisible && (Math.Abs((up - position).LongestAxis) < radius || Math.Abs((down - position).LongestAxis) < radius))
+            return SymmetryAxisDirection.Vertical;
+        return null;
+    }
+
+    private VecD ToVecD(Point pos) => new VecD(pos.X, pos.Y);
+
+    private SymmetryAxisDirection? capturedDirection;
+    private SymmetryAxisDirection? hoveredDirection;
+
+    private void UpdateHovered(SymmetryAxisDirection? direction)
+    {
+        Cursor = (hoveredDirection ?? capturedDirection) switch
+        {
+            SymmetryAxisDirection.Horizontal => new Cursor(StandardCursorType.SizeNorthSouth),
+            SymmetryAxisDirection.Vertical => new Cursor(StandardCursorType.SizeWestEast),
+            _ => new Cursor(StandardCursorType.Arrow)
+        };
+
+        if (hoveredDirection == direction)
+            return;
+
+        hoveredDirection = direction;
+        InvalidateVisual();
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        base.OnPointerPressed(e);
+
+        MouseButton button = e.GetCurrentPoint(this).Properties.PointerUpdateKind switch
+        {
+            PointerUpdateKind.LeftButtonPressed => MouseButton.Left,
+            PointerUpdateKind.RightButtonPressed => MouseButton.Right,
+            PointerUpdateKind.MiddleButtonPressed => MouseButton.Middle,
+            _ => MouseButton.None
+        };
+
+        if (button != MouseButton.Left)
+            return;
+
+        var rawPoint = e.GetPosition(this);
+        var pos = ToVecD(rawPoint);
+        var dir = IsTouchingHandle(pos);
+        if (dir is null)
+            return;
+        capturedDirection = dir.Value;
+        e.Pointer.Capture(this);
+        e.Handled = true;
+        CallSymmetryDragStartCommand(dir.Value);
+    }
+
+    protected override void OnPointerEntered(PointerEventArgs e)
+    {
+        base.OnPointerEntered(e);
+        var pos = ToVecD(e.GetPosition(this));
+        var dir = IsTouchingHandle(pos);
+        UpdateHovered(dir);
+    }
+
+    protected override void OnPointerExited(PointerEventArgs e)
+    {
+        UpdateHovered(null);
+    }
+
+    private void CallSymmetryDragCommand(SymmetryAxisDirection direction, double position)
+    {
+        SymmetryAxisDragInfo dragInfo = new(direction, position);
+        if (DragCommand is not null && DragCommand.CanExecute(dragInfo))
+            DragCommand.Execute(dragInfo);
+    }
+    private void CallSymmetryDragEndCommand(SymmetryAxisDirection direction)
+    {
+        if (DragEndCommand is not null && DragEndCommand.CanExecute(direction))
+            DragEndCommand.Execute(direction);
+    }
+    private void CallSymmetryDragStartCommand(SymmetryAxisDirection direction)
+    {
+        if (DragStartCommand is not null && DragStartCommand.CanExecute(direction))
+            DragStartCommand.Execute(direction);
+    }
+
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        base.OnPointerReleased(e);
+        if (e.InitialPressMouseButton != MouseButton.Left)
+            return;
+
+        if (capturedDirection is null)
+            return;
+
+        e.Pointer.Capture(null);
+
+        CallSymmetryDragEndCommand((SymmetryAxisDirection)capturedDirection);
+
+        capturedDirection = null;
+        UpdateHovered(IsTouchingHandle(ToVecD(e.GetPosition(this))));
+        // Not calling invalidate visual might result in ruler not disappearing when releasing the mouse over the canvas 
+        InvalidateVisual();
+        e.Handled = true;
+    }
+
+    protected void MouseMoved(PointerEventArgs e)
+    {
+        var rawPoint = e.GetPosition(this);
+        var pos = ToVecD(rawPoint);
+        UpdateHovered(IsTouchingHandle(pos));
+
+        if (capturedDirection is null)
+            return;
+        if (capturedDirection == SymmetryAxisDirection.Horizontal)
+        {
+            horizontalAxisY = Math.Round(Math.Clamp(pos.Y, 0, Bounds.Height) * 2) / 2;
+
+            if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+            {
+                double temp = Math.Round(horizontalAxisY / Bounds.Height * 8) / 8 * Bounds.Height;
+                horizontalAxisY = Math.Round(temp * 2) / 2;
+            }
+
+            CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, horizontalAxisY);
+        }
+        else if (capturedDirection == SymmetryAxisDirection.Vertical)
+        {
+            verticalAxisX = Math.Round(Math.Clamp(pos.X, 0, Bounds.Width) * 2) / 2;
+
+            if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+            {
+
+                double temp = Math.Round(verticalAxisX / Bounds.Width * 8) / 8 * Bounds.Width;
+                verticalAxisX = Math.Round(temp * 2) / 2;
+            }
+
+            CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
+        }
+
+        e.Handled = true;
+    }
+
+    private static void OnPositionUpdate(AvaloniaPropertyChangedEventArgs<double> e)
+    {
+        var self = (SymmetryOverlay)e.Sender;
+        self.horizontalAxisY = self.HorizontalAxisY;
+        self.verticalAxisX = self.VerticalAxisX;
+        self.InvalidateVisual();
+    }
+}