Browse Source

Symmetry changes/actions/changeinfos

Equbuxu 3 years ago
parent
commit
f481317ac0
18 changed files with 540 additions and 178 deletions
  1. 21 0
      src/PixiEditor.ChangeableDocument/Actions/Root/SetSymmetryState_Action.cs
  2. 11 0
      src/PixiEditor.ChangeableDocument/Actions/Root/SymmetryPosition/EndSetSymmetryPosition_Action.cs
  3. 26 0
      src/PixiEditor.ChangeableDocument/Actions/Root/SymmetryPosition/SetSymmetryPosition_Action.cs
  4. 7 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryPosition_ChangeInfo.cs
  5. 7 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryState_ChangeInfo.cs
  6. 4 0
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  7. 4 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  8. 69 0
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryPosition_UpdateableChange.cs
  9. 63 0
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryState_Change.cs
  10. 5 0
      src/PixiEditor.ChangeableDocument/Enums/SymmetryDirection.cs
  11. 0 5
      src/PixiEditorPrototype/CustomControls/SymmertyOverlay/SymmetryDirection.cs
  12. 0 149
      src/PixiEditorPrototype/CustomControls/SymmertyOverlay/SymmetryOverlay.cs
  13. 4 0
      src/PixiEditorPrototype/CustomControls/SymmetryOverlay/SymmetryDragInfo.cs
  14. 220 0
      src/PixiEditorPrototype/CustomControls/SymmetryOverlay/SymmetryOverlay.cs
  15. 23 0
      src/PixiEditorPrototype/Models/DocumentUpdater.cs
  16. 36 24
      src/PixiEditorPrototype/UserControls/Viewport/Viewport.xaml
  17. 34 0
      src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs
  18. 6 0
      src/PixiEditorPrototype/Views/MainWindow.xaml

+ 21 - 0
src/PixiEditor.ChangeableDocument/Actions/Root/SetSymmetryState_Action.cs

@@ -0,0 +1,21 @@
+using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Root;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.Actions.Root;
+public record class SetSymmetryState_Action : IMakeChangeAction
+{
+    public SetSymmetryState_Action(SymmetryDirection direction, bool state)
+    {
+        Direction = direction;
+        State = state;
+    }
+
+    public SymmetryDirection Direction { get; }
+    public bool State { get; }
+
+    Change IMakeChangeAction.CreateCorrespondingChange()
+    {
+        return new SymmetryState_Change(Direction, State);
+    }
+}

+ 11 - 0
src/PixiEditor.ChangeableDocument/Actions/Root/SymmetryPosition/EndSetSymmetryPosition_Action.cs

@@ -0,0 +1,11 @@
+using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Root;
+
+namespace PixiEditor.ChangeableDocument.Actions.Root.SymmetryPosition;
+public class EndSetSymmetryPosition_Action : IEndChangeAction
+{
+    bool IEndChangeAction.IsChangeTypeMatching(Change change)
+    {
+        return change is SymmetryPosition_UpdateableChange;
+    }
+}

+ 26 - 0
src/PixiEditor.ChangeableDocument/Actions/Root/SymmetryPosition/SetSymmetryPosition_Action.cs

@@ -0,0 +1,26 @@
+using PixiEditor.ChangeableDocument.Changes;
+using PixiEditor.ChangeableDocument.Changes.Root;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.Actions.Root.SymmetryPosition;
+public class SetSymmetryPosition_Action : IStartOrUpdateChangeAction
+{
+    public SetSymmetryPosition_Action(SymmetryDirection direction, int position)
+    {
+        Direction = direction;
+        Position = position;
+    }
+
+    public SymmetryDirection Direction { get; }
+    public int Position { get; }
+
+    UpdateableChange IStartOrUpdateChangeAction.CreateCorrespondingChange()
+    {
+        return new SymmetryPosition_UpdateableChange(Direction, Position);
+    }
+
+    void IStartOrUpdateChangeAction.UpdateCorrespodingChange(UpdateableChange change)
+    {
+        ((SymmetryPosition_UpdateableChange)change).Update(Position);
+    }
+}

+ 7 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryPosition_ChangeInfo.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
+public record class SymmetryPosition_ChangeInfo : IChangeInfo
+{
+    public SymmetryDirection Direction { get; init; }
+}

+ 7 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryState_ChangeInfo.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
+public record class SymmetryState_ChangeInfo : IChangeInfo
+{
+    public SymmetryDirection Direction { get; init; }
+}

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -16,6 +16,10 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     internal Folder StructureRoot { get; } = new() { GuidValue = Guid.Empty };
     internal Selection Selection { get; } = new();
     public Vector2i Size { get; set; } = DefaultSize;
+    public bool HorizontalSymmetryEnabled { get; set; }
+    public bool VerticalSymmetryEnabled { get; set; }
+    public int HorizontalSymmetryPosition { get; set; }
+    public int VerticalSymmetryPosition { get; set; }
 
     public void Dispose()
     {

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -7,6 +7,10 @@ public interface IReadOnlyDocument
     IReadOnlyFolder ReadOnlyStructureRoot { get; }
     IReadOnlySelection ReadOnlySelection { get; }
     Vector2i Size { get; }
+    bool HorizontalSymmetryEnabled { get; }
+    bool VerticalSymmetryEnabled { get; }
+    int HorizontalSymmetryPosition { get; }
+    int VerticalSymmetryPosition { get; }
     IReadOnlyStructureMember? FindMember(Guid guid);
     IReadOnlyStructureMember FindMemberOrThrow(Guid guid);
     (IReadOnlyStructureMember, IReadOnlyFolder) FindChildAndParentOrThrow(Guid guid);

+ 69 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryPosition_UpdateableChange.cs

@@ -0,0 +1,69 @@
+using PixiEditor.ChangeableDocument.Changeables;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+internal class SymmetryPosition_UpdateableChange : UpdateableChange
+{
+    private readonly SymmetryDirection direction;
+    private int newPos;
+    private int originalPos;
+
+    public SymmetryPosition_UpdateableChange(SymmetryDirection direction, int pos)
+    {
+        this.direction = direction;
+        newPos = pos;
+    }
+
+    public void Update(int pos)
+    {
+        newPos = pos;
+    }
+
+    public override void Initialize(Document target)
+    {
+        originalPos = direction switch
+        {
+            SymmetryDirection.Horizontal => target.HorizontalSymmetryPosition,
+            SymmetryDirection.Vertical => target.VerticalSymmetryPosition,
+            _ => throw new NotImplementedException(),
+        };
+    }
+
+    private void SetPosition(Document target, int position)
+    {
+        if (direction == SymmetryDirection.Horizontal)
+            target.HorizontalSymmetryPosition = position;
+        else if (direction == SymmetryDirection.Vertical)
+            target.VerticalSymmetryPosition = position;
+        else
+            throw new NotImplementedException();
+    }
+
+    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    {
+        ignoreInUndo = originalPos == newPos;
+        SetPosition(target, newPos);
+        return new SymmetryPosition_ChangeInfo() { Direction = direction };
+    }
+
+    public override IChangeInfo? ApplyTemporarily(Document target)
+    {
+        SetPosition(target, newPos);
+        return new SymmetryPosition_ChangeInfo() { Direction = direction };
+    }
+
+    public override IChangeInfo? Revert(Document target)
+    {
+        if (originalPos == newPos)
+            return null;
+        SetPosition(target, originalPos);
+        return new SymmetryPosition_ChangeInfo() { Direction = direction };
+    }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is SymmetryPosition_UpdateableChange;
+    }
+}

+ 63 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryState_Change.cs

@@ -0,0 +1,63 @@
+using PixiEditor.ChangeableDocument.Changeables;
+using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+internal class SymmetryState_Change : Change
+{
+    private readonly SymmetryDirection direction;
+    private readonly bool newEnabled;
+    private bool originalEnabled;
+
+    public SymmetryState_Change(SymmetryDirection direction, bool enabled)
+    {
+        this.direction = direction;
+        this.newEnabled = enabled;
+    }
+
+    public override void Initialize(Document target)
+    {
+        originalEnabled = direction switch
+        {
+            SymmetryDirection.Horizontal => target.HorizontalSymmetryEnabled,
+            SymmetryDirection.Vertical => target.VerticalSymmetryEnabled,
+            _ => throw new NotImplementedException(),
+        };
+    }
+
+    private void SetState(Document target, bool state)
+    {
+        if (direction == SymmetryDirection.Horizontal)
+            target.HorizontalSymmetryEnabled = state;
+        else if (direction == SymmetryDirection.Vertical)
+            target.VerticalSymmetryEnabled = state;
+        else
+            throw new NotImplementedException();
+    }
+
+    public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
+    {
+        if (originalEnabled == newEnabled)
+        {
+            ignoreInUndo = true;
+            return null;
+        }
+        SetState(target, newEnabled);
+        ignoreInUndo = false;
+        return new SymmetryState_ChangeInfo() { Direction = direction };
+    }
+
+    public override IChangeInfo? Revert(Document target)
+    {
+        if (originalEnabled == newEnabled)
+            return null;
+        SetState(target, originalEnabled);
+        return new SymmetryState_ChangeInfo() { Direction = direction };
+    }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is SymmetryState_Change;
+    }
+}

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

@@ -0,0 +1,5 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+public enum SymmetryDirection
+{
+    Horizontal, Vertical
+}

+ 0 - 5
src/PixiEditorPrototype/CustomControls/SymmertyOverlay/SymmetryDirection.cs

@@ -1,5 +0,0 @@
-namespace PixiEditorPrototype.CustomControls.SymmertyOverlay;
-internal enum SymmetryDirection
-{
-    Horizontal, Vertical
-}

+ 0 - 149
src/PixiEditorPrototype/CustomControls/SymmertyOverlay/SymmetryOverlay.cs

@@ -1,149 +0,0 @@
-using System;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
-using ChunkyImageLib.DataHolders;
-
-namespace PixiEditorPrototype.CustomControls.SymmertyOverlay;
-
-internal class SymmetryOverlay : Control
-{
-    public static readonly DependencyProperty HorizontalPositionProperty =
-        DependencyProperty.Register(nameof(HorizontalPosition), typeof(int), typeof(SymmetryOverlay),
-            new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public int HorizontalPosition
-    {
-        get => (int)GetValue(HorizontalPositionProperty);
-        set => SetValue(HorizontalPositionProperty, value);
-    }
-
-    public static readonly DependencyProperty VerticalPositionProperty =
-        DependencyProperty.Register(nameof(VerticalPosition), typeof(int), typeof(SymmetryOverlay),
-            new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public int VerticalPosition
-    {
-        get => (int)GetValue(VerticalPositionProperty);
-        set => SetValue(VerticalPositionProperty, value);
-    }
-
-    public static readonly DependencyProperty HorizontalVisibleProperty =
-        DependencyProperty.Register(nameof(HorizontalVisible), typeof(bool), typeof(SymmetryOverlay),
-            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public bool HorizontalVisible
-    {
-        get => (bool)GetValue(HorizontalVisibleProperty);
-        set => SetValue(HorizontalVisibleProperty, value);
-    }
-
-    public static readonly DependencyProperty VerticalVisibleProperty =
-        DependencyProperty.Register(nameof(VerticalVisible), typeof(bool), typeof(SymmetryOverlay),
-            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public bool VerticalVisible
-    {
-        get => (bool)GetValue(VerticalVisibleProperty);
-        set => SetValue(VerticalVisibleProperty, value);
-    }
-
-    public static readonly DependencyProperty ZoomboxScaleProperty =
-        DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(SymmetryOverlay),
-            new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsRender));
-
-    public double ZoomboxScale
-    {
-        get => (double)GetValue(ZoomboxScaleProperty);
-        set => SetValue(ZoomboxScaleProperty, value);
-    }
-
-    private const double HandleSize = 16;
-    private Pen borderPen = new Pen(Brushes.Black, 1.0);
-
-    protected override void OnRender(DrawingContext drawingContext)
-    {
-        base.OnRender(drawingContext);
-        if (!HorizontalVisible && !VerticalVisible)
-            return;
-
-        borderPen.Thickness = 1.0 / ZoomboxScale;
-        double radius = HandleSize / ZoomboxScale / 2;
-
-        if (HorizontalVisible)
-        {
-            drawingContext.DrawEllipse(Brushes.White, borderPen, new(-radius, HorizontalPosition), radius, radius);
-            drawingContext.DrawEllipse(Brushes.White, borderPen, new(ActualWidth + radius, HorizontalPosition), radius, radius);
-            drawingContext.DrawLine(borderPen, new(0, HorizontalPosition), new(ActualWidth, HorizontalPosition));
-        }
-        if (VerticalVisible)
-        {
-            drawingContext.DrawEllipse(Brushes.White, borderPen, new(VerticalPosition, -radius), radius, radius);
-            drawingContext.DrawEllipse(Brushes.White, borderPen, new(VerticalPosition, ActualHeight + radius), radius, radius);
-            drawingContext.DrawLine(borderPen, new(VerticalPosition, 0), new(VerticalPosition, ActualHeight));
-        }
-    }
-
-    protected override HitTestResult? HitTestCore(PointHitTestParameters hitTestParameters)
-    {
-        // prevent the line from blocking mouse input
-        if (IsTouchingHandle(ToVector2d(hitTestParameters.HitPoint)) is not null)
-            return new PointHitTestResult(this, hitTestParameters.HitPoint);
-        return null;
-    }
-
-    private SymmetryDirection? IsTouchingHandle(Vector2d position)
-    {
-        double radius = HandleSize / ZoomboxScale / 2;
-        Vector2d left = new(-radius, HorizontalPosition);
-        Vector2d right = new(ActualWidth + radius, HorizontalPosition);
-        Vector2d up = new(VerticalPosition, -radius);
-        Vector2d down = new(VerticalPosition, ActualHeight + radius);
-
-        if (HorizontalVisible && ((left - position).Length < radius || (right - position).Length < radius))
-            return SymmetryDirection.Horizontal;
-        if (VerticalVisible && ((up - position).Length < radius || (down - position).Length < radius))
-            return SymmetryDirection.Vertical;
-        return null;
-    }
-
-    private Vector2d ToVector2d(Point pos) => new Vector2d(pos.X, pos.Y);
-
-    public SymmetryDirection? capturedDirection;
-
-    protected override void OnMouseDown(MouseButtonEventArgs e)
-    {
-        base.OnMouseDown(e);
-
-        var pos = ToVector2d(e.GetPosition(this));
-        var dir = IsTouchingHandle(pos);
-        if (dir is null)
-            return;
-        capturedDirection = dir.Value;
-        CaptureMouse();
-    }
-
-    protected override void OnMouseUp(MouseButtonEventArgs e)
-    {
-        base.OnMouseUp(e);
-
-        if (capturedDirection is null)
-            return;
-        capturedDirection = null;
-        ReleaseMouseCapture();
-    }
-
-    protected override void OnMouseMove(MouseEventArgs e)
-    {
-        base.OnMouseMove(e);
-
-        if (capturedDirection is null)
-            return;
-        var pos = ToVector2d(e.GetPosition(this));
-        if (capturedDirection == SymmetryDirection.Horizontal)
-            HorizontalPosition = (int)Math.Round(Math.Clamp(pos.Y, 0, ActualHeight));
-        if (capturedDirection == SymmetryDirection.Vertical)
-            VerticalPosition = (int)Math.Round(Math.Clamp(pos.X, 0, ActualWidth));
-    }
-}

+ 4 - 0
src/PixiEditorPrototype/CustomControls/SymmetryOverlay/SymmetryDragInfo.cs

@@ -0,0 +1,4 @@
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditorPrototype.CustomControls.SymmetryOverlay;
+internal record class SymmetryDragInfo(SymmetryDirection Direction, int NewPosition);

+ 220 - 0
src/PixiEditorPrototype/CustomControls/SymmetryOverlay/SymmetryOverlay.cs

@@ -0,0 +1,220 @@
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Enums;
+
+namespace PixiEditorPrototype.CustomControls.SymmetryOverlay;
+
+internal class SymmetryOverlay : Control
+{
+    public static readonly DependencyProperty HorizontalPositionProperty =
+        DependencyProperty.Register(nameof(HorizontalPosition), typeof(int), typeof(SymmetryOverlay),
+            new(0, OnPositionUpdate));
+
+    public int HorizontalPosition
+    {
+        get => (int)GetValue(HorizontalPositionProperty);
+        set => SetValue(HorizontalPositionProperty, value);
+    }
+
+    public static readonly DependencyProperty VerticalPositionProperty =
+        DependencyProperty.Register(nameof(VerticalPosition), typeof(int), typeof(SymmetryOverlay),
+            new(0, OnPositionUpdate));
+
+    public int VerticalPosition
+    {
+        get => (int)GetValue(VerticalPositionProperty);
+        set => SetValue(VerticalPositionProperty, value);
+    }
+
+    public static readonly DependencyProperty HorizontalVisibleProperty =
+        DependencyProperty.Register(nameof(HorizontalVisible), typeof(bool), typeof(SymmetryOverlay),
+            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public bool HorizontalVisible
+    {
+        get => (bool)GetValue(HorizontalVisibleProperty);
+        set => SetValue(HorizontalVisibleProperty, value);
+    }
+
+    public static readonly DependencyProperty VerticalVisibleProperty =
+        DependencyProperty.Register(nameof(VerticalVisible), typeof(bool), typeof(SymmetryOverlay),
+            new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public bool VerticalVisible
+    {
+        get => (bool)GetValue(VerticalVisibleProperty);
+        set => SetValue(VerticalVisibleProperty, value);
+    }
+
+    public static readonly DependencyProperty ZoomboxScaleProperty =
+        DependencyProperty.Register(nameof(ZoomboxScale), typeof(double), typeof(SymmetryOverlay),
+            new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsRender));
+
+    public double ZoomboxScale
+    {
+        get => (double)GetValue(ZoomboxScaleProperty);
+        set => SetValue(ZoomboxScaleProperty, value);
+    }
+
+    public static readonly DependencyProperty DragCommandProperty =
+        DependencyProperty.Register(nameof(DragCommand), typeof(ICommand), typeof(SymmetryOverlay), new(null));
+
+    public ICommand? DragCommand
+    {
+        get => (ICommand)GetValue(DragCommandProperty);
+        set => SetValue(DragCommandProperty, value);
+    }
+
+    public static readonly DependencyProperty DragEndCommandProperty =
+        DependencyProperty.Register(nameof(DragEndCommand), typeof(ICommand), typeof(SymmetryOverlay), new(null));
+
+    public ICommand? DragEndCommand
+    {
+        get => (ICommand)GetValue(DragEndCommandProperty);
+        set => SetValue(DragEndCommandProperty, value);
+    }
+
+    private const double HandleSize = 16;
+    private PathGeometry handleGeometry = new()
+    {
+        FillRule = FillRule.Nonzero,
+        Figures = (PathFigureCollection?)new PathFigureCollectionConverter()
+            .ConvertFrom($"M -1 -0.5 L -0.5 -0.5 L 0 0 L -0.5 0.5 L -1 0.5 Z"),
+    };
+
+    private Pen borderPen = new Pen(Brushes.Black, 1.0);
+    private int horizontalPosition;
+    private int verticalPosition;
+
+    protected override void OnRender(DrawingContext drawingContext)
+    {
+        base.OnRender(drawingContext);
+        if (!HorizontalVisible && !VerticalVisible)
+            return;
+
+        borderPen.Thickness = 1.0 / ZoomboxScale;
+        handleGeometry.Transform = new ScaleTransform(HandleSize / ZoomboxScale, HandleSize / ZoomboxScale);
+
+        if (HorizontalVisible)
+        {
+            drawingContext.PushTransform(new TranslateTransform(0, horizontalPosition));
+            drawingContext.DrawGeometry(Brushes.White, borderPen, handleGeometry);
+            drawingContext.PushTransform(new RotateTransform(180, ActualWidth / 2, 0));
+            drawingContext.DrawGeometry(Brushes.White, borderPen, handleGeometry);
+            drawingContext.Pop();
+            drawingContext.Pop();
+            drawingContext.DrawLine(borderPen, new(0, horizontalPosition), new(ActualWidth, horizontalPosition));
+        }
+        if (VerticalVisible)
+        {
+            drawingContext.PushTransform(new RotateTransform(90));
+            drawingContext.PushTransform(new TranslateTransform(0, -verticalPosition));
+            drawingContext.DrawGeometry(Brushes.White, borderPen, handleGeometry);
+            drawingContext.PushTransform(new RotateTransform(180, ActualHeight / 2, 0));
+            drawingContext.DrawGeometry(Brushes.White, borderPen, handleGeometry);
+            drawingContext.Pop();
+            drawingContext.Pop();
+            drawingContext.Pop();
+            drawingContext.DrawLine(borderPen, new(verticalPosition, 0), new(verticalPosition, ActualHeight));
+        }
+    }
+
+    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 < ActualWidth && point.Y < ActualHeight)
+            return null;
+        return new PointHitTestResult(this, hitTestParameters.HitPoint);
+    }
+
+    private SymmetryDirection? IsTouchingHandle(Vector2d position)
+    {
+        double radius = HandleSize / ZoomboxScale / 2;
+        Vector2d left = new(-radius, horizontalPosition);
+        Vector2d right = new(ActualWidth + radius, horizontalPosition);
+        Vector2d up = new(verticalPosition, -radius);
+        Vector2d down = new(verticalPosition, ActualHeight + radius);
+
+        if (HorizontalVisible && ((left - position).Length < radius || (right - position).Length < radius))
+            return SymmetryDirection.Horizontal;
+        if (VerticalVisible && ((up - position).Length < radius || (down - position).Length < radius))
+            return SymmetryDirection.Vertical;
+        return null;
+    }
+
+    private Vector2d ToVector2d(Point pos) => new Vector2d(pos.X, pos.Y);
+
+    public SymmetryDirection? capturedDirection;
+
+    protected override void OnMouseDown(MouseButtonEventArgs e)
+    {
+        base.OnMouseDown(e);
+
+        var pos = ToVector2d(e.GetPosition(this));
+        var dir = IsTouchingHandle(pos);
+        if (dir is null)
+            return;
+        capturedDirection = dir.Value;
+        CaptureMouse();
+        e.Handled = true;
+    }
+
+    private void CallSymmetryDragCommand(SymmetryDirection direction, int position)
+    {
+        SymmetryDragInfo dragInfo = new(direction, position);
+        if (DragCommand is not null && DragCommand.CanExecute(dragInfo))
+            DragCommand.Execute(dragInfo);
+    }
+    private void CallSymmetryDragEndCommand(SymmetryDirection direction)
+    {
+        if (DragEndCommand is not null && DragEndCommand.CanExecute(direction))
+            DragEndCommand.Execute(direction);
+    }
+
+    protected override void OnMouseUp(MouseButtonEventArgs e)
+    {
+        base.OnMouseUp(e);
+
+        if (capturedDirection is null)
+            return;
+        ReleaseMouseCapture();
+
+        CallSymmetryDragEndCommand((SymmetryDirection)capturedDirection);
+
+        capturedDirection = null;
+        e.Handled = true;
+    }
+
+    protected override void OnMouseMove(MouseEventArgs e)
+    {
+        base.OnMouseMove(e);
+
+        if (capturedDirection is null)
+            return;
+        var pos = ToVector2d(e.GetPosition(this));
+        if (capturedDirection == SymmetryDirection.Horizontal)
+        {
+            horizontalPosition = (int)Math.Round(Math.Clamp(pos.Y, 0, ActualHeight));
+            CallSymmetryDragCommand((SymmetryDirection)capturedDirection, horizontalPosition);
+        }
+        else if (capturedDirection == SymmetryDirection.Vertical)
+        {
+            verticalPosition = (int)Math.Round(Math.Clamp(pos.X, 0, ActualWidth));
+            CallSymmetryDragCommand((SymmetryDirection)capturedDirection, verticalPosition);
+        }
+        e.Handled = true;
+    }
+
+    private static void OnPositionUpdate(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+    {
+        var self = (SymmetryOverlay)obj;
+        self.horizontalPosition = self.HorizontalPosition;
+        self.verticalPosition = self.VerticalPosition;
+        self.InvalidateVisual();
+    }
+}

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

@@ -9,6 +9,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditorPrototype.ViewModels;
 using SkiaSharp;
 
@@ -70,9 +71,31 @@ internal class DocumentUpdater
             case Selection_ChangeInfo info:
                 ProcessSelection(info);
                 break;
+            case SymmetryState_ChangeInfo info:
+                ProcessSymmetryState(info);
+                break;
+            case SymmetryPosition_ChangeInfo info:
+                ProcessSymmetryPosition(info);
+                break;
         }
     }
 
+    private void ProcessSymmetryPosition(SymmetryPosition_ChangeInfo info)
+    {
+        if (info.Direction == SymmetryDirection.Horizontal)
+            doc.RaisePropertyChanged(nameof(doc.HorizontalSymmetryPosition));
+        else if (info.Direction == SymmetryDirection.Vertical)
+            doc.RaisePropertyChanged(nameof(doc.VerticalSymmetryPosition));
+    }
+
+    private void ProcessSymmetryState(SymmetryState_ChangeInfo info)
+    {
+        if (info.Direction == SymmetryDirection.Horizontal)
+            doc.RaisePropertyChanged(nameof(doc.HorizontalSymmetryEnabled));
+        else if (info.Direction == SymmetryDirection.Vertical)
+            doc.RaisePropertyChanged(nameof(doc.VerticalSymmetryEnabled));
+    }
+
     private void ProcessSelection(Selection_ChangeInfo info)
     {
         doc.RaisePropertyChanged(nameof(doc.SelectionPath));

+ 36 - 24
src/PixiEditorPrototype/UserControls/Viewport/Viewport.xaml

@@ -8,7 +8,7 @@
              xmlns:zoombox="clr-namespace:PixiEditor.Zoombox;assembly=PixiEditor.Zoombox"
              xmlns:to="clr-namespace:PixiEditorPrototype.CustomControls.TransformOverlay"
              xmlns:cust="clr-namespace:PixiEditorPrototype.CustomControls"
-             xmlns:sym="clr-namespace:PixiEditorPrototype.CustomControls.SymmertyOverlay"
+             xmlns:sym="clr-namespace:PixiEditorPrototype.CustomControls.SymmetryOverlay"
              xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
              xmlns:conv="clr-namespace:PixiEditorPrototype.Converters"
              mc:Ignorable="d"
@@ -18,21 +18,24 @@
         <conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
     </UserControl.Resources>
     <Grid>
-        <zoombox:Zoombox x:Name="zoombox" UseTouchGestures="True"
-                             Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
-                             Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
-                             RealDimensions="{Binding RealDimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
-                             Dimensions="{Binding Dimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
-                             ZoomMode="{Binding ZoomMode, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}" 
-                             FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
-                             FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
-            <Border BorderThickness="1" Background="White" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center"
-                    DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+        <zoombox:Zoombox 
+            x:Name="zoombox" UseTouchGestures="True"
+            Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            RealDimensions="{Binding RealDimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            Dimensions="{Binding Dimensions, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            ZoomMode="{Binding ZoomMode, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=TwoWay}" 
+            FlipX="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}"
+            FlipY="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
+            <Border 
+                BorderThickness="1" Background="White" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center"
+                DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}}">
                 <Grid>
-                    <Image Focusable="True"
-                               Width="{Binding Document.Width}" Height="{Binding Document.Height}"
-                               Source="{Binding TargetBitmap}"
-                               RenderOptions.BitmapScalingMode="NearestNeighbor">
+                    <Image 
+                        Focusable="True"
+                        Width="{Binding Document.Width}" Height="{Binding Document.Height}"
+                        Source="{Binding TargetBitmap}"
+                        RenderOptions.BitmapScalingMode="NearestNeighbor">
                         <i:Interaction.Triggers>
                             <i:EventTrigger EventName="MouseDown">
                                 <i:InvokeCommandAction Command="{Binding MouseDownCommand}" PassEventArgsToCommand="True"/>
@@ -45,16 +48,25 @@
                             </i:EventTrigger>
                         </i:Interaction.Triggers>
                     </Image>
-                    <sym:SymmetryOverlay HorizontalVisible="True" VerticalVisible="True" ZoomboxScale="{Binding Zoombox.Scale}"/>
+                    <sym:SymmetryOverlay 
+                        ZoomboxScale="{Binding Zoombox.Scale}"
+                        HorizontalVisible="{Binding Document.HorizontalSymmetryEnabled}"
+                        VerticalVisible="{Binding Document.VerticalSymmetryEnabled}"
+                        HorizontalPosition="{Binding Document.HorizontalSymmetryPosition, Mode=OneWay}"
+                        VerticalPosition="{Binding Document.VerticalSymmetryPosition, Mode=OneWay}"
+                        DragCommand="{Binding Document.DragSymmetryCommand}"
+                        DragEndCommand="{Binding Document.EndDragSymmetryCommand}"
+                        />
                     <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}"
-                                            RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners}"
-                                            CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
-                                            SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
-                                            InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
-                                            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}"
+                        RequestedCorners="{Binding Document.TransformViewModel.RequestedCorners}"
+                        CornerFreedom="{Binding Document.TransformViewModel.CornerFreedom}"
+                        SideFreedom="{Binding Document.TransformViewModel.SideFreedom}"
+                        InternalState="{Binding Document.TransformViewModel.InternalState, Mode=TwoWay}"
+                        ZoomboxScale="{Binding Zoombox.Scale}"/>
                 </Grid>
             </Border>
         </zoombox:Zoombox>

+ 34 - 0
src/PixiEditorPrototype/ViewModels/DocumentViewModel.cs

@@ -14,9 +14,11 @@ using PixiEditor.ChangeableDocument.Actions.Drawing.Rectangle;
 using PixiEditor.ChangeableDocument.Actions.Drawing.Selection;
 using PixiEditor.ChangeableDocument.Actions.Properties;
 using PixiEditor.ChangeableDocument.Actions.Root;
+using PixiEditor.ChangeableDocument.Actions.Root.SymmetryPosition;
 using PixiEditor.ChangeableDocument.Actions.Structure;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditorPrototype.CustomControls.SymmetryOverlay;
 using PixiEditorPrototype.Models;
 using SkiaSharp;
 
@@ -67,11 +69,25 @@ internal class DocumentViewModel : INotifyPropertyChanged
     public RelayCommand? ToggleLockTransparencyCommand { get; }
     public RelayCommand? ApplyTransformCommand { get; }
     public RelayCommand? PasteImageCommand { get; }
+    public RelayCommand? DragSymmetryCommand { get; }
+    public RelayCommand? EndDragSymmetryCommand { get; }
 
     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 int HorizontalSymmetryPosition => Helpers.Tracker.Document.HorizontalSymmetryPosition;
+    public int VerticalSymmetryPosition => Helpers.Tracker.Document.VerticalSymmetryPosition;
+    public bool HorizontalSymmetryEnabled
+    {
+        get => Helpers.Tracker.Document.HorizontalSymmetryEnabled;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new SetSymmetryState_Action(SymmetryDirection.Horizontal, value));
+    }
+    public bool VerticalSymmetryEnabled
+    {
+        get => Helpers.Tracker.Document.VerticalSymmetryEnabled;
+        set => Helpers.ActionAccumulator.AddFinishedActions(new SetSymmetryState_Action(SymmetryDirection.Vertical, value));
+    }
 
     public Dictionary<ChunkResolution, SKSurface> Surfaces { get; set; } = new();
 
@@ -110,6 +126,8 @@ internal class DocumentViewModel : INotifyPropertyChanged
         ToggleLockTransparencyCommand = new RelayCommand(ToggleLockTransparency);
         PasteImageCommand = new RelayCommand(PasteImage);
         ApplyTransformCommand = new RelayCommand(ApplyTransform);
+        DragSymmetryCommand = new RelayCommand(DragSymmetry);
+        EndDragSymmetryCommand = new RelayCommand(EndDragSymmetry);
 
         foreach (var bitmap in Bitmaps)
         {
@@ -133,6 +151,22 @@ internal class DocumentViewModel : INotifyPropertyChanged
 
     private ShapeCorners lastShape = new ShapeCorners();
     private ShapeData lastShapeData = new();
+
+    private void DragSymmetry(object? obj)
+    {
+        if (obj is null)
+            return;
+        var info = (SymmetryDragInfo)obj;
+        Helpers.ActionAccumulator.AddActions(new SetSymmetryPosition_Action(info.Direction, info.NewPosition));
+    }
+
+    private void EndDragSymmetry(object? obj)
+    {
+        if (obj is null)
+            return;
+        Helpers.ActionAccumulator.AddFinishedActions(new EndSetSymmetryPosition_Action());
+    }
+
     public void StartUpdateRectangle(ShapeData data)
     {
         if (SelectedStructureMember is null)

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

@@ -165,6 +165,12 @@
                 <RadioButton GroupName="zoomboxMode" Margin="5,0" IsChecked="{Binding RotateZoombox, Mode=OneWayToSource}">Rotate</RadioButton>
                 <CheckBox x:Name="flipXCheckbox" Margin="5, 0">Flip X</CheckBox>
                 <CheckBox x:Name="flipYCheckbox" Margin="5, 0">Flip Y</CheckBox>
+                <CheckBox 
+                    x:Name="horizontalSymmetryCheckbox" Margin="5,0" 
+                    IsChecked="{Binding ActiveDocument.HorizontalSymmetryEnabled}">Hor Sym</CheckBox>
+                <CheckBox 
+                    x:Name="verticalSymmetryCheckbox" Margin="5,0" 
+                    IsChecked="{Binding ActiveDocument.VerticalSymmetryEnabled}">Ver Sym</CheckBox>
             </StackPanel>
         </Border>
         <Grid>