diff --git a/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs b/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs index 2572e1b3..ed5e2292 100644 --- a/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs +++ b/Ink Canvas/Controls/ImageSelectionOverlay.xaml.cs @@ -2,6 +2,7 @@ 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 @@ -17,23 +18,21 @@ namespace Ink_Canvas.Controls public class ImageResizeDeltaEventArgs : EventArgs { public ImageResizeCorner Corner { get; } - public Point CurrentCanvasPoint { get; } - public Point StartCanvasPoint { get; } + public Vector CanvasDelta { get; } public bool LockAspectRatio { get; } - public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Point start, Point current, bool lockAspect) + public ImageResizeDeltaEventArgs(ImageResizeCorner corner, Vector canvasDelta, bool lockAspect) { Corner = corner; - StartCanvasPoint = start; - CurrentCanvasPoint = current; + CanvasDelta = canvasDelta; LockAspectRatio = lockAspect; } } public class ImageMoveDeltaEventArgs : EventArgs { - public Vector Delta { get; } - public ImageMoveDeltaEventArgs(Vector delta) { Delta = delta; } + public Vector CanvasDelta { get; } + public ImageMoveDeltaEventArgs(Vector delta) { CanvasDelta = delta; } } public class ImageRotateDeltaEventArgs : EventArgs @@ -56,7 +55,8 @@ namespace Ink_Canvas.Controls public IInputElement CoordinateSource { get; set; } - private Point _rotationCenter; + private Point _rotationCenterCanvas; + private readonly RotateTransform _overlayRotation = new RotateTransform(0); private bool _isResizing; private bool _isRotating; @@ -68,6 +68,7 @@ namespace Ink_Canvas.Controls public ImageSelectionOverlay() { InitializeComponent(); + RenderTransform = _overlayRotation; TopLeftHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopLeft, e, TopLeftHandle); TopRightHandle.MouseLeftButtonDown += (s, e) => BeginResize(ImageResizeCorner.TopRight, e, TopRightHandle); @@ -93,39 +94,49 @@ namespace Ink_Canvas.Controls MoveSurface.MouseLeftButtonUp += EndMove; } - public void UpdateFrame(Rect canvasBounds, double rotationAngleDegrees) + /// + /// Position overlay so its logical rect (width × height) is centered at centerCanvas, + /// then rotated by rotationAngleDegrees around that center to match the target element. + /// + public void UpdateFrame(Point centerCanvas, double width, double height, double rotationAngleDegrees) { - if (canvasBounds.Width <= 0 || canvasBounds.Height <= 0) return; + if (width <= 0 || height <= 0) return; - _rotationCenter = new Point(canvasBounds.Left + canvasBounds.Width / 2, - canvasBounds.Top + canvasBounds.Height / 2); + _rotationCenterCanvas = centerCanvas; - Margin = new Thickness(canvasBounds.Left, canvasBounds.Top, 0, 0); - Width = canvasBounds.Width; - Height = canvasBounds.Height; + double left = centerCanvas.X - width / 2; + double top = centerCanvas.Y - height / 2; + Margin = new Thickness(left, top, 0, 0); + Width = width; + Height = height; - FrameBorder.Width = canvasBounds.Width; - FrameBorder.Height = canvasBounds.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 = canvasBounds.Width; - MoveSurface.Height = canvasBounds.Height; + 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, canvasBounds.Width - 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, canvasBounds.Height - h); - System.Windows.Controls.Canvas.SetLeft(BottomRightHandle, canvasBounds.Width - h); - System.Windows.Controls.Canvas.SetTop(BottomRightHandle, canvasBounds.Height - 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 = canvasBounds.Width / 2; + double midX = width / 2; System.Windows.Controls.Canvas.SetLeft(RotationHandle, midX - rh); System.Windows.Controls.Canvas.SetTop(RotationHandle, -RotationHandleOffset - rh); @@ -155,8 +166,9 @@ namespace Ink_Canvas.Controls 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, _lastPoint, current, lockAspect)); + ResizeDelta?.Invoke(this, new ImageResizeDeltaEventArgs(_activeCorner, delta, lockAspect)); _lastPoint = current; e.Handled = true; } @@ -240,8 +252,8 @@ namespace Ink_Canvas.Controls private double AngleFromCenter(Point p) { - double dx = p.X - _rotationCenter.X; - double dy = p.Y - _rotationCenter.Y; + double dx = p.X - _rotationCenterCanvas.X; + double dy = p.Y - _rotationCenterCanvas.Y; return Math.Atan2(dy, dx) * 180.0 / Math.PI; } } diff --git a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs index 9027e594..cb58b784 100644 --- a/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs +++ b/Ink Canvas/MainWindow_cs/MW_ElementsControls.cs @@ -513,6 +513,14 @@ namespace Ink_Canvas var rotateTransform = transformGroup.Children.OfType().FirstOrDefault(); if (rotateTransform != null) { + // 绕元素视觉中心旋转,避免旋转后位置乱飘 + double w = element.ActualWidth > 0 ? element.ActualWidth : element.Width; + double h = element.ActualHeight > 0 ? element.ActualHeight : element.Height; + if (!double.IsNaN(w) && !double.IsNaN(h)) + { + rotateTransform.CenterX = w / 2; + rotateTransform.CenterY = h / 2; + } rotateTransform.Angle += angle; } } @@ -2399,8 +2407,7 @@ namespace Ink_Canvas { if (ImageSelectionOverlay == null || element == null) return; EnsureImageOverlayHooks(); - Rect bounds = GetElementActualBounds(element); - ImageSelectionOverlay.UpdateFrame(bounds, GetElementRotationAngle(element)); + UpdateImageResizeHandlesPosition(default); ImageSelectionOverlay.Visibility = Visibility.Visible; } catch (Exception ex) @@ -2422,13 +2429,42 @@ namespace Ink_Canvas } } + // elementBounds parameter is ignored — overlay needs the UNROTATED bounds of the element, + // computed directly from the element's own state so rotation never distorts the frame. private void UpdateImageResizeHandlesPosition(Rect elementBounds) { try { - if (ImageSelectionOverlay == null) return; - double angle = currentSelectedElement != null ? GetElementRotationAngle(currentSelectedElement) : 0; - ImageSelectionOverlay.UpdateFrame(elementBounds, angle); + if (ImageSelectionOverlay == null || currentSelectedElement == null) return; + + var element = currentSelectedElement; + double w = element.ActualWidth > 0 ? element.ActualWidth : element.Width; + double h = element.ActualHeight > 0 ? element.ActualHeight : element.Height; + if (double.IsNaN(w) || double.IsNaN(h) || w <= 0 || h <= 0) return; + + double left = InkCanvas.GetLeft(element); + double top = InkCanvas.GetTop(element); + if (double.IsNaN(left)) left = 0; + if (double.IsNaN(top)) top = 0; + + double scaleX = 1, scaleY = 1, translateX = 0, translateY = 0, angle = 0; + if (element.RenderTransform is TransformGroup tg) + { + var st = tg.Children.OfType().FirstOrDefault(); + var tt = tg.Children.OfType().FirstOrDefault(); + var rt = tg.Children.OfType().FirstOrDefault(); + if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; } + if (tt != null) { translateX = tt.X; translateY = tt.Y; } + if (rt != null) angle = rt.Angle; + } + + double scaledW = w * scaleX; + double scaledH = h * scaleY; + // Local (pre-rotation) center in canvas coordinates + double localCenterX = left + translateX + scaledW / 2; + double localCenterY = top + translateY + scaledH / 2; + + ImageSelectionOverlay.UpdateFrame(new Point(localCenterX, localCenterY), scaledW, scaledH, angle); } catch (Exception ex) { @@ -2446,12 +2482,22 @@ namespace Ink_Canvas return 0; } + // Rotate a canvas-space vector back into the element's local (unrotated) space. + private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees) + { + double rad = -angleDegrees * Math.PI / 180.0; + double cos = Math.Cos(rad); + double sin = Math.Sin(rad); + return new Vector(canvasDelta.X * cos - canvasDelta.Y * sin, + canvasDelta.X * sin + canvasDelta.Y * cos); + } + private void ImageSelectionOverlay_ResizeDelta(object sender, ImageResizeDeltaEventArgs e) { try { if (!IsBitmapLikeCanvasElement(currentSelectedElement)) return; - ResizeImageByCorner(currentSelectedElement, e.StartCanvasPoint, e.CurrentCanvasPoint, e.Corner, e.LockAspectRatio); + ResizeImageByCorner(currentSelectedElement, e.CanvasDelta, e.Corner, e.LockAspectRatio); } catch (Exception ex) { @@ -2469,11 +2515,14 @@ namespace Ink_Canvas var tt = tg.Children.OfType().FirstOrDefault(); if (tt != null) { - tt.X += e.Delta.X; - tt.Y += e.Delta.Y; + // Translate is applied before Rotate, so convert canvas delta to local space. + double angle = GetElementRotationAngle(currentSelectedElement); + var local = CanvasVectorToLocal(e.CanvasDelta, angle); + tt.X += local.X; + tt.Y += local.Y; } } - UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement)); + UpdateImageResizeHandlesPosition(default); if (BorderImageSelectionControl?.Visibility == Visibility.Visible) UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -2489,7 +2538,7 @@ namespace Ink_Canvas { if (currentSelectedElement == null) return; ApplyRotateTransform(currentSelectedElement, e.AngleDelta); - UpdateImageResizeHandlesPosition(GetElementActualBounds(currentSelectedElement)); + UpdateImageResizeHandlesPosition(default); if (BorderImageSelectionControl?.Visibility == Visibility.Visible) UpdateImageSelectionToolbarPosition(currentSelectedElement); } @@ -2499,84 +2548,111 @@ namespace Ink_Canvas } } - private void ResizeImageByCorner(FrameworkElement element, Point startPoint, Point currentPoint, + private void ResizeImageByCorner(FrameworkElement element, Vector canvasDelta, ImageResizeCorner corner, bool lockAspect) { if (!(element.RenderTransform is TransformGroup transformGroup)) return; var scaleTransform = transformGroup.Children.OfType().FirstOrDefault(); var translateTransform = transformGroup.Children.OfType().FirstOrDefault(); + var rotateTransform = transformGroup.Children.OfType().FirstOrDefault(); if (scaleTransform == null || translateTransform == null) return; - Rect currentBounds = GetElementActualBounds(element); - if (currentBounds.Width <= 0 || currentBounds.Height <= 0) return; + double angle = rotateTransform?.Angle ?? 0; + double baseW = element.ActualWidth > 0 ? element.ActualWidth : element.Width; + double baseH = element.ActualHeight > 0 ? element.ActualHeight : element.Height; + if (double.IsNaN(baseW) || double.IsNaN(baseH) || baseW <= 0 || baseH <= 0) return; - double deltaX = currentPoint.X - startPoint.X; - double deltaY = currentPoint.Y - startPoint.Y; + double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0; + double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0; - double scaleX = 1.0, scaleY = 1.0, translateX = 0, translateY = 0; + double curW = baseW * scaleTransform.ScaleX; + double curH = baseH * scaleTransform.ScaleY; + // Drag delta → local (unrotated) space so corner tracks cursor under any rotation. + Vector local = CanvasVectorToLocal(canvasDelta, angle); + + double newW = curW, newH = curH; + double pivotFracX = 0, pivotFracY = 0; // opposite corner as fraction of scaled box switch (corner) { case ImageResizeCorner.TopLeft: - scaleX = (currentBounds.Width - deltaX) / currentBounds.Width; - scaleY = (currentBounds.Height - deltaY) / currentBounds.Height; - translateX = deltaX; - translateY = deltaY; + newW = curW - local.X; newH = curH - local.Y; + pivotFracX = 1; pivotFracY = 1; break; case ImageResizeCorner.TopRight: - scaleX = (currentBounds.Width + deltaX) / currentBounds.Width; - scaleY = (currentBounds.Height - deltaY) / currentBounds.Height; - translateY = deltaY; + newW = curW + local.X; newH = curH - local.Y; + pivotFracX = 0; pivotFracY = 1; break; case ImageResizeCorner.BottomLeft: - scaleX = (currentBounds.Width - deltaX) / currentBounds.Width; - scaleY = (currentBounds.Height + deltaY) / currentBounds.Height; - translateX = deltaX; + newW = curW - local.X; newH = curH + local.Y; + pivotFracX = 1; pivotFracY = 0; break; case ImageResizeCorner.BottomRight: - scaleX = (currentBounds.Width + deltaX) / currentBounds.Width; - scaleY = (currentBounds.Height + deltaY) / currentBounds.Height; + newW = curW + local.X; newH = curH + local.Y; + pivotFracX = 0; pivotFracY = 0; break; } - if (lockAspect) + if (lockAspect && curW > 0 && curH > 0) { - double uniform = Math.Min(scaleX, scaleY); - scaleX = uniform; - scaleY = uniform; - // recompute translate so opposite corner stays put - double wNew = currentBounds.Width * uniform; - double hNew = currentBounds.Height * uniform; - switch (corner) - { - case ImageResizeCorner.TopLeft: - translateX = currentBounds.Width - wNew; - translateY = currentBounds.Height - hNew; - break; - case ImageResizeCorner.TopRight: - translateX = 0; - translateY = currentBounds.Height - hNew; - break; - case ImageResizeCorner.BottomLeft: - translateX = currentBounds.Width - wNew; - translateY = 0; - break; - } + double uniform = Math.Min(newW / curW, newH / curH); + newW = curW * uniform; + newH = curH * uniform; } - scaleX = Math.Max(0.1, Math.Min(scaleX, 5.0)); - scaleY = Math.Max(0.1, Math.Min(scaleY, 5.0)); + // Clamp final scale + double newScaleX = newW / baseW; + double newScaleY = newH / baseH; + newScaleX = Math.Max(0.1, Math.Min(newScaleX, 10.0)); + newScaleY = Math.Max(0.1, Math.Min(newScaleY, 10.0)); + newW = baseW * newScaleX; + newH = baseH * newScaleY; - scaleTransform.ScaleX *= scaleX; - scaleTransform.ScaleY *= scaleY; - translateTransform.X += translateX; - translateTransform.Y += translateY; + double tx = translateTransform.X; + double ty = translateTransform.Y; - UpdateImageResizeHandlesPosition(GetElementActualBounds(element)); + // Pivot canvas position BEFORE the change + Point pivotBefore = ComputeCornerCanvas(pivotFracX * curW, pivotFracY * curH, + curW, curH, left, top, tx, ty, angle); + // Pivot canvas position AFTER the scale change, assuming translate unchanged + Point pivotAfter = ComputeCornerCanvas(pivotFracX * newW, pivotFracY * newH, + newW, newH, left, top, tx, ty, angle); + + // Shift translate in local space to bring pivotAfter back to pivotBefore + Vector canvasDrift = pivotBefore - pivotAfter; + Vector localShift = CanvasVectorToLocal(canvasDrift, angle); + translateTransform.X += localShift.X; + translateTransform.Y += localShift.Y; + + scaleTransform.ScaleX = newScaleX; + scaleTransform.ScaleY = newScaleY; + + UpdateImageResizeHandlesPosition(default); if (BorderImageSelectionControl?.Visibility == Visibility.Visible) UpdateImageSelectionToolbarPosition(element); } + // Canvas position of local point (lx, ly) inside the scaled box of size (sW, sH), + // given element placement (left, top), translate (tx, ty) and rotation angle (deg). + // Rotation pivot is the visual center: pre-rotate (tx + sW/2, ty + sH/2). + private static Point ComputeCornerCanvas(double lx, double ly, double sW, double sH, + double left, double top, double tx, double ty, + double angleDeg) + { + double preX = tx + lx; + double preY = ty + ly; + double cx = tx + sW / 2; + double cy = ty + sH / 2; + double rad = angleDeg * Math.PI / 180.0; + double cos = Math.Cos(rad); + double sin = Math.Sin(rad); + double relX = preX - cx; + double relY = preY - cy; + double rotX = relX * cos - relY * sin + cx; + double rotY = relX * sin + relY * cos + cy; + return new Point(left + rotX, top + rotY); + } + #endregion } }