Files
community/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs
2026-05-02 09:23:19 +08:00

260 lines
9.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Ink_Canvas.Controls
{
public enum ImageResizeCorner
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
public class ImageResizeDeltaEventArgs : EventArgs
{
public ImageResizeCorner Corner { get; }
public Vector CanvasDelta { get; }
public bool LockAspectRatio { get; }
public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Vector canvasDelta, bool lockAspect)
{
Corner = corner;
CanvasDelta = canvasDelta;
LockAspectRatio = lockAspect;
}
}
public class ImageMoveDeltaEventArgs : EventArgs
{
public Vector CanvasDelta { get; }
public ImageMoveDeltaEventArgs(Vector delta) { CanvasDelta = delta; }
}
public class ImageRotateDeltaEventArgs : EventArgs
{
public double AngleDelta { get; }
public ImageRotateDeltaEventArgs(double angleDelta) { AngleDelta = angleDelta; }
}
public partial class ImageSelectionOverlay : UserControl
{
private const double HandleSize = 12;
private const double RotationHandleSize = 14;
private const double RotationHandleOffset = 28;
public event EventHandler<ImageResizeDeltaEventArgs> ResizeDelta;
public event EventHandler<ImageMoveDeltaEventArgs> MoveDelta;
public event EventHandler<ImageRotateDeltaEventArgs> RotateDelta;
public event EventHandler InteractionStarted;
public event EventHandler InteractionEnded;
public IInputElement CoordinateSource { get; set; }
private Point _rotationCenterCanvas;
private readonly RotateTransform _overlayRotation = new RotateTransform(0);
private bool _isResizing;
private bool _isRotating;
private bool _isMoving;
private ImageResizeCorner _activeCorner;
private Point _lastPoint;
private double _lastRotationAngle;
public ImageSelectionOverlay()
{
InitializeComponent();
RenderTransform = _overlayRotation;
TopLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopLeft, e, TopLeftHandle);
TopRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopRight, e, TopRightHandle);
BottomLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomLeft, e, BottomLeftHandle);
BottomRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.BottomRight, e, BottomRightHandle);
TopLeftHandle.MouseMove += ResizeMove;
TopRightHandle.MouseMove += ResizeMove;
BottomLeftHandle.MouseMove += ResizeMove;
BottomRightHandle.MouseMove += ResizeMove;
TopLeftHandle.MouseLeftButtonUp += EndResize;
TopRightHandle.MouseLeftButtonUp += EndResize;
BottomLeftHandle.MouseLeftButtonUp += EndResize;
BottomRightHandle.MouseLeftButtonUp += EndResize;
RotationHandle.MouseLeftButtonDown += BeginRotate;
RotationHandle.MouseMove += RotateMove;
RotationHandle.MouseLeftButtonUp += EndRotate;
MoveSurface.MouseLeftButtonDown += BeginMove;
MoveSurface.MouseMove += MoveMove;
MoveSurface.MouseLeftButtonUp += EndMove;
}
/// <summary>
/// Position overlay so its logical rect (width × height) is centered at centerCanvas,
/// then rotated by rotationAngleDegrees around that center to match the target element.
/// </summary>
public void UpdateFrame(Point centerCanvas, double width, double height, double rotationAngleDegrees)
{
if (width <= 0 || height <= 0) return;
_rotationCenterCanvas = centerCanvas;
double left = centerCanvas.X - width / 2;
double top = centerCanvas.Y - height / 2;
Margin = new Thickness(left, top, 0, 0);
Width = width;
Height = height;
RenderTransformOrigin = new Point(0, 0);
_overlayRotation.Angle = rotationAngleDegrees;
_overlayRotation.CenterX = width / 2;
_overlayRotation.CenterY = height / 2;
FrameBorder.Width = width;
FrameBorder.Height = height;
System.Windows.Controls.Canvas.SetLeft(FrameBorder, 0);
System.Windows.Controls.Canvas.SetTop(FrameBorder, 0);
MoveSurface.Width = width;
MoveSurface.Height = height;
System.Windows.Controls.Canvas.SetLeft(MoveSurface, 0);
System.Windows.Controls.Canvas.SetTop(MoveSurface, 0);
double h = HandleSize / 2;
System.Windows.Controls.Canvas.SetLeft(TopLeftHandle, -h);
System.Windows.Controls.Canvas.SetTop(TopLeftHandle, -h);
System.Windows.Controls.Canvas.SetLeft(TopRightHandle, width - h);
System.Windows.Controls.Canvas.SetTop(TopRightHandle, -h);
System.Windows.Controls.Canvas.SetLeft(BottomLeftHandle, -h);
System.Windows.Controls.Canvas.SetTop(BottomLeftHandle, height - h);
System.Windows.Controls.Canvas.SetLeft(BottomRightHandle, width - h);
System.Windows.Controls.Canvas.SetTop(BottomRightHandle, height - h);
double rh = RotationHandleSize / 2;
double midX = width / 2;
System.Windows.Controls.Canvas.SetLeft(RotationHandle, midX - rh);
System.Windows.Controls.Canvas.SetTop(RotationHandle, -RotationHandleOffset - rh);
RotationLine.X1 = midX;
RotationLine.Y1 = 0;
RotationLine.X2 = midX;
RotationLine.Y2 = -RotationHandleOffset;
}
private IInputElement GetSource() => CoordinateSource ?? (IInputElement)Parent;
private void BeginResize(ImageResizeCorner corner, MouseButtonEventArgs e, Ellipse handle)
{
var source = GetSource();
if (source == null) return;
_isResizing = true;
_activeCorner = corner;
_lastPoint = e.GetPosition(source);
handle.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void ResizeMove(object sender, MouseEventArgs e)
{
if (!_isResizing || !(sender is Ellipse handle) || !handle.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var current = e.GetPosition(source);
var delta = current - _lastPoint;
bool lockAspect = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
ResizeDelta?.Invoke(this, new ImageResizeDeltaEventArgs(_activeCorner, delta, lockAspect));
_lastPoint = current;
e.Handled = true;
}
private void EndResize(object sender, MouseButtonEventArgs e)
{
if (!_isResizing) return;
if (sender is Ellipse handle) handle.ReleaseMouseCapture();
_isResizing = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void BeginRotate(object sender, MouseButtonEventArgs e)
{
var source = GetSource();
if (source == null) return;
_isRotating = true;
var p = e.GetPosition(source);
_lastRotationAngle = AngleFromCenter(p);
RotationHandle.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void RotateMove(object sender, MouseEventArgs e)
{
if (!_isRotating || !RotationHandle.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var p = e.GetPosition(source);
double angle = AngleFromCenter(p);
double delta = angle - _lastRotationAngle;
if (delta > 180) delta -= 360;
else if (delta < -180) delta += 360;
_lastRotationAngle = angle;
RotateDelta?.Invoke(this, new ImageRotateDeltaEventArgs(delta));
e.Handled = true;
}
private void EndRotate(object sender, MouseButtonEventArgs e)
{
if (!_isRotating) return;
RotationHandle.ReleaseMouseCapture();
_isRotating = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void BeginMove(object sender, MouseButtonEventArgs e)
{
var source = GetSource();
if (source == null) return;
_isMoving = true;
_lastPoint = e.GetPosition(source);
MoveSurface.CaptureMouse();
InteractionStarted?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private void MoveMove(object sender, MouseEventArgs e)
{
if (!_isMoving || !MoveSurface.IsMouseCaptured) return;
var source = GetSource();
if (source == null) return;
var current = e.GetPosition(source);
var delta = current - _lastPoint;
_lastPoint = current;
MoveDelta?.Invoke(this, new ImageMoveDeltaEventArgs(delta));
e.Handled = true;
}
private void EndMove(object sender, MouseButtonEventArgs e)
{
if (!_isMoving) return;
MoveSurface.ReleaseMouseCapture();
_isMoving = false;
InteractionEnded?.Invoke(this, EventArgs.Empty);
e.Handled = true;
}
private double AngleFromCenter(Point p)
{
double dx = p.X - _rotationCenterCanvas.X;
double dy = p.Y - _rotationCenterCanvas.Y;
return Math.Atan2(dy, dx) * 180.0 / Math.PI;
}
}
}