Bläddra i källkod

Overlay pointer handling

Krzysztof Krysiński 1 år sedan
förälder
incheckning
c05cb3879b

+ 0 - 18
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml

@@ -120,7 +120,6 @@
         </overlays:TogglableFlyout>
         <visuals:Scene
             Focusable="False" Name="scene"
-            RenderTransformOrigin="0,0"
             ZIndex="1"
             Width="{Binding RealDimensions.X, ElementName=vpUc}"
             Height="{Binding RealDimensions.Y, ElementName=vpUc}"
@@ -222,23 +221,6 @@
                 DragEndCommand="{xaml:Command PixiEditor.Document.EndDragSymmetry, UseProvided=True}"
                 DragStartCommand="{xaml:Command PixiEditor.Document.StartDragSymmetry, UseProvided=True}"
                 FlowDirection="LeftToRight" />
-            <selectionOverlay:SelectionOverlay
-                Focusable="False"
-                ShowFill="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels:MainVM}, Converter={converters:IsSelectionToolConverter}}"
-                Path="{Binding Document.SelectionPathBindable}"
-                ZoomboxScale="{Binding #zoombox.Scale}"
-                FlowDirection="LeftToRight" />
-            <brushShapeOverlay:BrushShapeOverlay
-                Name="brushShapeOverlay"
-                Focusable="False"
-                IsHitTestVisible="False"
-                IsVisible="{Binding !Document.TransformViewModel.TransformActive}"
-                ZoomboxScale="{Binding #zoombox.Scale}"
-                MouseEventSource="{Binding #vpUc.BackgroundGrid, Mode=OneTime}"
-                MouseReference="{Binding #vpUc.MainImage, Mode=OneTime}"
-                BrushSize="{Binding ToolsSubViewModel.ActiveBasicToolbar.ToolSize, Source={viewModels:MainVM}}"
-                BrushShape="{Binding ToolsSubViewModel.ActiveTool.BrushShape, Source={viewModels:MainVM}, FallbackValue={x:Static brushShapeOverlay:BrushShape.Hidden}}"
-                FlowDirection="LeftToRight" />
             <transformOverlay:TransformOverlay
                 Focusable="False"
                 Cursor="Arrow"

+ 26 - 3
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -13,6 +13,7 @@ using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 using PixiEditor.AvaloniaUI.Helpers.UI;
+using PixiEditor.AvaloniaUI.Models.Commands.XAML;
 using PixiEditor.AvaloniaUI.Models.Controllers.InputDevice;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Position;
@@ -297,6 +298,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private GridLines gridLinesOverlay;
     private SelectionOverlay selectionOverlay;
+    private SymmetryOverlay symmetryOverlay;
 
     static Viewport()
     {
@@ -328,8 +330,12 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         selectionOverlay = new SelectionOverlay();
         BindSelectionOverlay();
 
+        symmetryOverlay = new SymmetryOverlay();
+        BindSymmetryOverlay();
+
         ActiveOverlays.Add(gridLinesOverlay);
         ActiveOverlays.Add(selectionOverlay);
+        ActiveOverlays.Add(symmetryOverlay);
     }
 
     public Panel? MainImage => zoombox != null ? (Panel?)((Grid?)((Border?)zoombox.AdditionalContent)?.Child)?.Children[0] : null;
@@ -414,9 +420,6 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private void BindSelectionOverlay()
     {
-        //    ShowFill="{Binding ToolsSubViewModel.ActiveTool, Source={viewModels:MainVM}, Converter={converters:IsSelectionToolConverter}}"
-        //Path="{Binding Document.SelectionPathBindable}"
-
         Binding showFillBinding = new()
         {
             Source = this,
@@ -445,6 +448,26 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         selectionOverlay.Bind(IsVisibleProperty, isVisibleBinding);
     }
 
+    private void BindSymmetryOverlay()
+    {
+        Binding sizeBinding = new() { Source = this, Path = "Document.SizeBindable", Mode = BindingMode.OneWay };
+        Binding isHitTestVisibleBinding = new() { Source = this, Path = "ZoomMode", Converter = new ZoomModeToHitTestVisibleConverter(), Mode = BindingMode.OneWay };
+        Binding horizontalAxisVisibleBinding = new() { Source = this, Path = "Document.HorizontalSymmetryAxisEnabledBindable", Mode = BindingMode.OneWay };
+        Binding verticalAxisVisibleBinding = new() { Source = this, Path = "Document.VerticalSymmetryAxisEnabledBindable", Mode = BindingMode.OneWay };
+        Binding horizontalAxisYBinding = new() { Source = this, Path = "Document.HorizontalSymmetryAxisYBindable", Mode = BindingMode.OneWay };
+        Binding verticalAxisXBinding = new() { Source = this, Path = "Document.VerticalSymmetryAxisXBindable", Mode = BindingMode.OneWay };
+
+        symmetryOverlay.Bind(SymmetryOverlay.SizeProperty, sizeBinding);
+        symmetryOverlay.Bind(IsHitTestVisibleProperty, isHitTestVisibleBinding);
+        symmetryOverlay.Bind(SymmetryOverlay.HorizontalAxisVisibleProperty, horizontalAxisVisibleBinding);
+        symmetryOverlay.Bind(SymmetryOverlay.VerticalAxisVisibleProperty, verticalAxisVisibleBinding);
+        symmetryOverlay.Bind(SymmetryOverlay.HorizontalAxisYProperty, horizontalAxisYBinding);
+        symmetryOverlay.Bind(SymmetryOverlay.VerticalAxisXProperty, verticalAxisXBinding);
+        symmetryOverlay.DragCommand = (ICommand)new Command("PixiEditor.Document.DragSymmetry") { UseProvided = true }.ProvideValue(null);
+        symmetryOverlay.DragEndCommand = (ICommand)new Command("PixiEditor.Document.EndDragSymmetry") { UseProvided = true }.ProvideValue(null);
+        symmetryOverlay.DragStartCommand = (ICommand)new Command("PixiEditor.Document.StartDragSymmetry") { UseProvided = true }.ProvideValue(null);
+    }
+
     private void OnImageSizeChanged(object? sender, DocumentSizeChangedEventArgs e)
     {
         PropertyChanged?.Invoke(this, new(nameof(TargetBitmap)));

+ 41 - 1
src/PixiEditor.AvaloniaUI/Views/Overlays/Overlay.cs

@@ -1,12 +1,15 @@
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Data;
+using Avalonia.Input;
 using PixiEditor.AvaloniaUI.Views.Overlays.Handles;
+using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.AvaloniaUI.Views.Overlays;
 
-public abstract class Overlay : Decorator
+public abstract class Overlay : Decorator // TODO: Maybe make it not avalonia element
 {
     public List<Handle> Handles { get; } = new();
 
@@ -19,6 +22,8 @@ public abstract class Overlay : Decorator
         set => SetValue(ZoomScaleProperty, value);
     }
 
+    public event Action? RefreshRequested;
+
     public Overlay()
     {
         ZoomScaleProperty.Changed.Subscribe(OnZoomboxScaleChanged);
@@ -63,4 +68,39 @@ public abstract class Overlay : Decorator
             }
         }
     }
+
+    public virtual bool TestHit(VecD point)
+    {
+        return Handles.Any(handle => handle.HandleRect.ContainsInclusive(new VecD(point.X, point.Y)));
+    }
+
+    public void Refresh()
+    {
+        RefreshRequested?.Invoke(); // For scene hosted overlays
+        InvalidateVisual(); // For elements in visual tree
+    }
+
+    public virtual void PointerEnteredOverlay(OverlayPointerArgs args)
+    {
+    }
+
+    public virtual void PointerExitedOverlay(OverlayPointerArgs args)
+    {
+
+    }
+
+    public virtual void PointerMovedOverlay(OverlayPointerArgs args)
+    {
+
+    }
+
+    public virtual void PointerPressedOverlay(OverlayPointerArgs args)
+    {
+
+    }
+
+    public virtual void PointerReleasedOverlay(OverlayPointerArgs args)
+    {
+
+    }
 }

+ 15 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/OverlayPointerArgs.cs

@@ -0,0 +1,15 @@
+using Avalonia.Input;
+using PixiEditor.AvaloniaUI.Views.Overlays.Pointers;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Views.Overlays;
+
+public struct OverlayPointerArgs
+{
+    public VecD Point { get; set; }
+    public KeyModifiers Modifiers { get; set; }
+    public MouseButton PointerButton { get; set; }
+    public MouseButton InitialPressMouseButton { get; set; }
+
+    public IOverlayPointer Pointer { get; set; }
+}

+ 6 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/Pointers/IOverlayPointer.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.AvaloniaUI.Views.Overlays.Pointers;
+
+public interface IOverlayPointer
+{
+    public void Capture(Overlay? overlay);
+}

+ 21 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/Pointers/MouseOverlayPointer.cs

@@ -0,0 +1,21 @@
+using Avalonia.Input;
+using PixiEditor.AvaloniaUI.Views.Visuals;
+
+namespace PixiEditor.AvaloniaUI.Views.Overlays.Pointers;
+
+internal class MouseOverlayPointer : IOverlayPointer
+{
+    IPointer pointer;
+    private Action<Overlay?, IPointer> captureAction;
+
+    public MouseOverlayPointer(IPointer pointer, Action<Overlay?, IPointer> captureAction)
+    {
+        this.pointer = pointer;
+        this.captureAction = captureAction;
+    }
+
+    public void Capture(Overlay? overlay)
+    {
+        captureAction(overlay, pointer);
+    }
+}

+ 72 - 125
src/PixiEditor.AvaloniaUI/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -51,15 +51,6 @@ internal class SymmetryOverlay : Overlay
         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));
 
@@ -87,15 +78,9 @@ internal class SymmetryOverlay : Overlay
         set => SetValue(DragStartCommandProperty, value);
     }
 
-    static SymmetryOverlay()
-    {
-        AffectsRender<SymmetryOverlay>(HorizontalAxisVisibleProperty);
-        AffectsRender<SymmetryOverlay>(VerticalAxisVisibleProperty);
-        AffectsRender<SymmetryOverlay>(ZoomboxScaleProperty);
-
-        HorizontalAxisYProperty.Changed.Subscribe(OnPositionUpdate);
-        VerticalAxisXProperty.Changed.Subscribe(OnPositionUpdate);
-    }
+    private SymmetryAxisDirection? capturedDirection;
+    private SymmetryAxisDirection? hoveredDirection;
+    public static readonly StyledProperty<VecI> SizeProperty = AvaloniaProperty.Register<SymmetryOverlay, VecI>(nameof(Size));
 
     private const double HandleSize = 12;
     private Geometry handleGeometry = Handle.GetHandleGeometry("MarkerHandle");
@@ -110,7 +95,7 @@ internal class SymmetryOverlay : Overlay
     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 PenThickness => 1.0 / ZoomScale;
 
     public VecI Size    
     {
@@ -120,30 +105,16 @@ internal class SymmetryOverlay : Overlay
 
     private double horizontalAxisY;
     private double verticalAxisX;
-    private Point pointerPosition;
-
-    private MouseUpdateController? mouseUpdateController;
+    private VecD pointerPosition;
 
-    public SymmetryOverlay()
-    {
-        Loaded += OnLoaded;
-        Unloaded += OnUnloaded;
-    }
-
-    private void OnUnloaded(object? sender, RoutedEventArgs e)
-    {
-        mouseUpdateController?.Dispose();
-    }
-
-    private void OnLoaded(object? sender, RoutedEventArgs e)
+    static SymmetryOverlay()
     {
-        mouseUpdateController = new MouseUpdateController(this, MouseMoved);
-        PointerEntered += OnPointerEntered;
-    }
+        AffectsRender<SymmetryOverlay>(HorizontalAxisVisibleProperty);
+        AffectsRender<SymmetryOverlay>(VerticalAxisVisibleProperty);
+        AffectsRender<SymmetryOverlay>(ZoomScaleProperty);
 
-    private void OnPointerEntered(object? sender, PointerEventArgs e)
-    {
-        pointerPosition = e.GetPosition(this);
+        HorizontalAxisYProperty.Changed.Subscribe(OnPositionUpdate);
+        VerticalAxisXProperty.Changed.Subscribe(OnPositionUpdate);
     }
 
     public override void Render(DrawingContext drawingContext)
@@ -157,7 +128,7 @@ internal class SymmetryOverlay : Overlay
         checkerWhite.Thickness = PenThickness;
         rulerPen.Thickness = PenThickness;
 
-        handleGeometry.Transform = new ScaleTransform(HandleSize / ZoomboxScale, HandleSize / ZoomboxScale);
+        handleGeometry.Transform = new ScaleTransform(HandleSize / ZoomScale, HandleSize / ZoomScale);
 
         if (HorizontalAxisVisible)
         {
@@ -228,7 +199,7 @@ internal class SymmetryOverlay : Overlay
         string text = upper ? $"{start - horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({(start - horizontalAxisY) / Size.Y * 100:F1}%)‎" : $"{horizontalAxisY}{new LocalizedString("PIXEL_UNIT")} ({horizontalAxisY / Size.Y * 100:F1}%)‎";
 
         var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
-            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomboxScale, Brushes.White);
+            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomScale, Brushes.White);
 
         if (Size.Y < formattedText.Height * 2.5 || horizontalAxisY == (int)Size.Y && upper || horizontalAxisY == 0 && !upper)
         {
@@ -260,7 +231,7 @@ internal class SymmetryOverlay : Overlay
         string text = right ? $"{start - verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({(start - verticalAxisX) / Size.X * 100:F1}%)‎" : $"{verticalAxisX}{new LocalizedString("PIXEL_UNIT")} ({verticalAxisX / Size.X * 100:F1}%)‎";
 
         var formattedText = new FormattedText(text, CultureInfo.GetCultureInfo("en-us"),
-            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomboxScale, Brushes.White);
+            ILocalizationProvider.Current.CurrentLanguage.FlowDirection, new Typeface("Segeo UI"), 14.0 / ZoomScale, Brushes.White);
 
         if (Size.X < formattedText.Width * 2.5 || verticalAxisX == (int)Size.X && right || verticalAxisX == 0 && !right)
         {
@@ -279,20 +250,14 @@ internal class SymmetryOverlay : Overlay
         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)
+    public override bool TestHit(VecD point)
     {
-        // prevent the line from blocking mouse input
-        var point = hitTestParameters.HitPoint;
-        if (point.X > 0 && point.Y > 0 && point.X < Size.X && point.Y < Size.Y)
-            return null;
-
-        return new PointHitTestResult(this, hitTestParameters.HitPoint);
-    }*/
+        return IsTouchingHandle(point) is not null;
+    }
 
     private SymmetryAxisDirection? IsTouchingHandle(VecD position)
     {
-        double radius = HandleSize * 4 / ZoomboxScale / 2;
+        double radius = HandleSize * 4 / ZoomScale / 2;
         VecD left = new(-radius, horizontalAxisY);
         VecD right = new(Size.X + radius, horizontalAxisY);
         VecD up = new(verticalAxisX, -radius);
@@ -307,10 +272,6 @@ internal class SymmetryOverlay : Overlay
 
     private VecD ToVecD(Point pos) => new VecD(pos.X, pos.Y);
 
-    private SymmetryAxisDirection? capturedDirection;
-    private SymmetryAxisDirection? hoveredDirection;
-    public static readonly StyledProperty<VecI> SizeProperty = AvaloniaProperty.Register<SymmetryOverlay, VecI>("Size");
-
     private void UpdateHovered(SymmetryAxisDirection? direction)
     {
         Cursor = (hoveredDirection ?? capturedDirection) switch
@@ -324,92 +285,40 @@ internal class SymmetryOverlay : Overlay
             return;
 
         hoveredDirection = direction;
-        InvalidateVisual();
+        Refresh();
     }
 
-    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    public override void PointerPressedOverlay(OverlayPointerArgs args)
     {
-        base.OnPointerPressed(e);
-
-        MouseButton button = e.GetMouseButton(this);
-
-        if (button != MouseButton.Left)
+        if (args.PointerButton != MouseButton.Left)
             return;
 
-        var rawPoint = e.GetPosition(this);
-        var pos = ToVecD(rawPoint);
-        var dir = IsTouchingHandle(pos);
+        var dir = IsTouchingHandle(args.Point);
         if (dir is null)
             return;
         capturedDirection = dir.Value;
-        e.Pointer.Capture(this);
-        e.Handled = true;
+        args.Pointer.Capture(this);
         CallSymmetryDragStartCommand(dir.Value);
     }
 
-    protected override void OnPointerEntered(PointerEventArgs e)
+    public override void PointerEnteredOverlay(OverlayPointerArgs args)
     {
-        base.OnPointerEntered(e);
-        var pos = ToVecD(e.GetPosition(this));
-        var dir = IsTouchingHandle(pos);
+        pointerPosition = args.Point;
+        var dir = IsTouchingHandle(pointerPosition);
         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)
+    public override void PointerMovedOverlay(OverlayPointerArgs args)
     {
-        var rawPoint = e.GetPosition(this);
-        var pos = ToVecD(rawPoint);
-        UpdateHovered(IsTouchingHandle(pos));
+        UpdateHovered(IsTouchingHandle(args.Point));
 
         if (capturedDirection is null)
             return;
         if (capturedDirection == SymmetryAxisDirection.Horizontal)
         {
-            horizontalAxisY = Math.Round(Math.Clamp(pos.Y, 0, Size.Y) * 2) / 2;
+            horizontalAxisY = Math.Round(Math.Clamp(args.Point.Y, 0, Size.Y) * 2) / 2;
 
-            if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+            if (args.Modifiers.HasFlag(KeyModifiers.Shift))
             {
                 double temp = Math.Round(horizontalAxisY / Size.Y * 8) / 8 * Size.Y;
                 horizontalAxisY = Math.Round(temp * 2) / 2;
@@ -419,9 +328,9 @@ internal class SymmetryOverlay : Overlay
         }
         else if (capturedDirection == SymmetryAxisDirection.Vertical)
         {
-            verticalAxisX = Math.Round(Math.Clamp(pos.X, 0, Size.X) * 2) / 2;
+            verticalAxisX = Math.Round(Math.Clamp(args.Point.X, 0, Size.X) * 2) / 2;
 
-            if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+            if (args.Modifiers.HasFlag(KeyModifiers.Control))
             {
 
                 double temp = Math.Round(verticalAxisX / Size.X * 8) / 8 * Size.X;
@@ -430,8 +339,46 @@ internal class SymmetryOverlay : Overlay
 
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
         }
+    }
+
+    public override void PointerExitedOverlay(OverlayPointerArgs args)
+    {
+        UpdateHovered(null);
+    }
+
+    public override void PointerReleasedOverlay(OverlayPointerArgs e)
+    {
+        if (e.InitialPressMouseButton != MouseButton.Left)
+            return;
+
+        if (capturedDirection is null)
+            return;
 
-        e.Handled = true;
+        e.Pointer.Capture(null);
+
+        CallSymmetryDragEndCommand((SymmetryAxisDirection)capturedDirection);
+
+        capturedDirection = null;
+        UpdateHovered(IsTouchingHandle(e.Point));
+        // Not calling invalidate visual might result in ruler not disappearing when releasing the mouse over the canvas
+        Refresh();
+    }
+
+    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);
     }
 
     private static void OnPositionUpdate(AvaloniaPropertyChangedEventArgs<double> e)
@@ -439,6 +386,6 @@ internal class SymmetryOverlay : Overlay
         var self = (SymmetryOverlay)e.Sender;
         self.horizontalAxisY = self.HorizontalAxisY;
         self.verticalAxisX = self.VerticalAxisX;
-        self.InvalidateVisual();
+        self.Refresh();
     }
 }

+ 179 - 27
src/PixiEditor.AvaloniaUI/Views/Visuals/Scene.cs

@@ -14,6 +14,7 @@ using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.Views.Overlays;
+using PixiEditor.AvaloniaUI.Views.Overlays.Pointers;
 using PixiEditor.AvaloniaUI.Views.Overlays.TransformOverlay;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
@@ -50,8 +51,9 @@ internal class Scene : Control, ICustomHitTest
     public static readonly StyledProperty<bool> FadeOutProperty = AvaloniaProperty.Register<Scene, bool>(
         nameof(FadeOut), false);
 
-    public static readonly StyledProperty<ObservableCollection<Overlay>> ActiveOverlaysProperty = AvaloniaProperty.Register<Scene, ObservableCollection<Overlay>>(
-        nameof(ActiveOverlays));
+    public static readonly StyledProperty<ObservableCollection<Overlay>> ActiveOverlaysProperty =
+        AvaloniaProperty.Register<Scene, ObservableCollection<Overlay>>(
+            nameof(ActiveOverlays));
 
     public static readonly StyledProperty<string> CheckerImagePathProperty = AvaloniaProperty.Register<Scene, string>(
         nameof(CheckerImagePath));
@@ -117,14 +119,19 @@ internal class Scene : Control, ICustomHitTest
     }
 
     private Bitmap? checkerBitmap;
+    private bool captured;
+    private Overlay? capturedOverlay;
 
     static Scene()
     {
         AffectsRender<Scene>(BoundsProperty, WidthProperty, HeightProperty, ScaleProperty, AngleProperty, FlipXProperty,
             FlipYProperty, ContentPositionProperty, DocumentProperty, SurfaceProperty);
         BoundsProperty.Changed.AddClassHandler<Scene>(BoundsChanged);
-        FlipXProperty.Changed.AddClassHandler<Scene>(RequestRendering);
-        FlipYProperty.Changed.AddClassHandler<Scene>(RequestRendering);
+        ContentPositionProperty.Changed.AddClassHandler<Scene>(Rerender);
+        ScaleProperty.Changed.AddClassHandler<Scene>(Rerender);
+        FlipXProperty.Changed.AddClassHandler<Scene>(Rerender);
+        FlipYProperty.Changed.AddClassHandler<Scene>(Rerender);
+        AngleProperty.Changed.AddClassHandler<Scene>(Rerender);
         FadeOutProperty.Changed.AddClassHandler<Scene>(FadeOutChanged);
         CheckerImagePathProperty.Changed.AddClassHandler<Scene>(CheckerImagePathChanged);
         ActiveOverlaysProperty.Changed.AddClassHandler<Scene>(ActiveOverlaysChanged);
@@ -144,8 +151,6 @@ internal class Scene : Control, ICustomHitTest
         if (Surface == null || Document == null) return;
 
         float finalScale = CalculateFinalScale();
-        context.PushTransform(Matrix.CreateTranslation(ContentPosition.X, ContentPosition.Y));
-        context.PushTransform(Matrix.CreateScale(finalScale, finalScale));
 
         float angle = (float)Angle;
         if (FlipX)
@@ -158,25 +163,153 @@ internal class Scene : Control, ICustomHitTest
             angle = 360 - angle;
         }
 
+        context.PushTransform(Matrix.CreateTranslation(ContentPosition.X, ContentPosition.Y));
+        context.PushTransform(Matrix.CreateScale(finalScale, finalScale));
         context.PushTransform(Matrix.CreateRotation(MathUtil.AngleToRadians(angle)));
         context.PushTransform(Matrix.CreateScale(FlipX ? -1 : 1, FlipY ? -1 : 1));
 
-        var operation = new DrawSceneOperation(Surface, Document, ContentPosition, finalScale, Angle, FlipX, FlipY, Bounds,
+        var operation = new DrawSceneOperation(Surface, Document, ContentPosition, finalScale, Angle, FlipX, FlipY,
+            Bounds,
             Opacity, (SKBitmap)checkerBitmap.Native);
         context.Custom(operation);
 
+
         if (ActiveOverlays != null)
         {
             foreach (Overlay overlay in ActiveOverlays)
             {
                 overlay.ZoomScale = finalScale;
-                if(!overlay.IsVisible) continue;
+                if (!overlay.IsVisible) continue;
 
                 overlay.Render(context);
+                Cursor = overlay.Cursor;
+            }
+        }
+    }
+
+    protected override void OnPointerEntered(PointerEventArgs e)
+    {
+        base.OnPointerEntered(e);
+        if (ActiveOverlays != null)
+        {
+            if (captured)
+            {
+                capturedOverlay?.PointerEnteredOverlay(ConstructPointerArgs(e));
+            }
+            else
+            {
+                foreach (Overlay overlay in ActiveOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.PointerEnteredOverlay(ConstructPointerArgs(e));
+                }
+            }
+        }
+    }
+
+    protected override void OnPointerMoved(PointerEventArgs e)
+    {
+        base.OnPointerMoved(e);
+        if (ActiveOverlays != null)
+        {
+            if (captured)
+            {
+                capturedOverlay?.PointerMovedOverlay(ConstructPointerArgs(e));
+            }
+            else
+            {
+                foreach (Overlay overlay in ActiveOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.PointerMovedOverlay(ConstructPointerArgs(e));
+                }
+            }
+        }
+    }
+
+    protected override void OnPointerPressed(PointerPressedEventArgs e)
+    {
+        base.OnPointerPressed(e);
+        if (ActiveOverlays != null)
+        {
+            if (captured)
+            {
+                capturedOverlay?.PointerPressedOverlay(ConstructPointerArgs(e));
+            }
+            else
+            {
+                foreach (Overlay overlay in ActiveOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.PointerPressedOverlay(ConstructPointerArgs(e));
+                }
+            }
+        }
+    }
+
+    protected override void OnPointerExited(PointerEventArgs e)
+    {
+        base.OnPointerExited(e);
+        if (ActiveOverlays != null)
+        {
+            foreach (Overlay overlay in ActiveOverlays)
+            {
+                if (!overlay.IsVisible) continue;
+                overlay.PointerExitedOverlay(ConstructPointerArgs(e));
+            }
+        }
+    }
+
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        base.OnPointerExited(e);
+        if (ActiveOverlays != null)
+        {
+            if (captured)
+            {
+                capturedOverlay?.PointerReleasedOverlay(ConstructPointerArgs(e));
+            }
+            else
+            {
+                foreach (Overlay overlay in ActiveOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.PointerReleasedOverlay(ConstructPointerArgs(e));
+                }
             }
         }
     }
 
+    private OverlayPointerArgs ConstructPointerArgs(PointerEventArgs e)
+    {
+        return new OverlayPointerArgs
+        {
+            Point = ToCanvasSpace(e.GetPosition(this)),
+            Modifiers = e.KeyModifiers,
+            Pointer = new MouseOverlayPointer(e.Pointer, CaptureOverlay),
+            PointerButton = e.GetMouseButton(this),
+            InitialPressMouseButton = e is PointerReleasedEventArgs released ? released.InitialPressMouseButton : MouseButton.None
+        };
+    }
+
+    private VecD ToCanvasSpace(Point scenePosition)
+    {
+        Matrix transform = CalculateTransformMatrix();
+        Point transformed = transform.Invert().Transform(scenePosition);
+        return new VecD(transformed.X, transformed.Y);
+    }
+
+    private Matrix CalculateTransformMatrix()
+    {
+        Matrix transform = Matrix.Identity;
+        float finalScale = CalculateFinalScale();
+        transform = transform.Append(Matrix.CreateRotation(MathUtil.AngleToRadians((float)Angle)));
+        transform = transform.Append(Matrix.CreateScale(FlipX ? -1 : 1, FlipY ? -1 : 1));
+        transform = transform.Append(Matrix.CreateScale(finalScale, finalScale));
+        transform = transform.Append(Matrix.CreateTranslation(ContentPosition.X, ContentPosition.Y));
+        return transform;
+    }
+
     private float CalculateFinalScale()
     {
         var scaleUniform = CalculateResolutionScale();
@@ -192,14 +325,31 @@ internal class Scene : Control, ICustomHitTest
         return scaleUniform;
     }
 
+    private void CaptureOverlay(Overlay? overlay, IPointer pointer)
+    {
+        if(ActiveOverlays == null) return;
+        if (overlay == null)
+        {
+            pointer.Capture(null);
+            captured = false;
+            return;
+        }
+
+        if(overlay != null && !ActiveOverlays.Contains(overlay)) return;
+
+        pointer.Capture(this);
+        capturedOverlay = overlay;
+        captured = true;
+    }
+
     private static void BoundsChanged(Scene sender, AvaloniaPropertyChangedEventArgs e)
     {
         sender.InvalidateVisual();
     }
 
-    private static void RequestRendering(Scene sender, AvaloniaPropertyChangedEventArgs e)
+    private static void Rerender(Scene scene, AvaloniaPropertyChangedEventArgs e)
     {
-        sender.InvalidateVisual();
+        scene.InvalidateVisual();
     }
 
     private static void FadeOutChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
@@ -214,6 +364,7 @@ internal class Scene : Control, ICustomHitTest
         {
             oldOverlays.CollectionChanged -= scene.OverlayCollectionChanged;
         }
+
         if (e.NewValue is ObservableCollection<Overlay> newOverlays)
         {
             newOverlays.CollectionChanged += scene.OverlayCollectionChanged;
@@ -223,6 +374,21 @@ internal class Scene : Control, ICustomHitTest
     private void OverlayCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
         InvalidateVisual();
+        if(e.OldItems != null)
+        {
+            foreach (Overlay overlay in e.OldItems)
+            {
+                overlay.RefreshRequested -= InvalidateVisual;
+            }
+        }
+
+        if(e.NewItems != null)
+        {
+            foreach (Overlay overlay in e.NewItems)
+            {
+                overlay.RefreshRequested += InvalidateVisual;
+            }
+        }
     }
 
     private static void CheckerImagePathChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
@@ -239,21 +405,18 @@ internal class Scene : Control, ICustomHitTest
 
     bool ICustomHitTest.HitTest(Point point)
     {
-        //TODO: Overlays
-        return false;
-        /*if (ActiveOverlays == null) return false;
+        if (ActiveOverlays == null) return false;
 
         foreach (Overlay overlay in ActiveOverlays)
         {
-            Point pointInOverlay = point - overlay.Bounds.Position;
-            if (overlay.InputHitTest(pointInOverlay) != null)
+            VecD pointInOverlay = ToCanvasSpace(point);
+            if (overlay.TestHit(pointInOverlay))
             {
                 return true;
             }
         }
 
         return false;
-    }*/
     }
 }
 
@@ -313,17 +476,6 @@ internal class DrawSceneOperation : SkiaDrawOperation
             return;
         }
 
-        float angle = (float)Angle;
-        if (FlipX)
-        {
-            angle = 360 - angle;
-        }
-
-        if (FlipY)
-        {
-            angle = 360 - angle;
-        }
-
         DrawCheckerboard(canvas, surfaceRectToRender);
 
         using Image snapshot = Surface.DrawingSurface.Snapshot(surfaceRectToRender);