improve:图片选中

This commit is contained in:
2026-05-02 09:41:28 +08:00
parent 9691263aa5
commit 2b8dcfdc86
+161 -69
View File
@@ -510,19 +510,22 @@ namespace Ink_Canvas
{ {
if (element.RenderTransform is TransformGroup transformGroup) if (element.RenderTransform is TransformGroup transformGroup)
{ {
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
var rotateTransform = transformGroup.Children.OfType<RotateTransform>().FirstOrDefault(); var rotateTransform = transformGroup.Children.OfType<RotateTransform>().FirstOrDefault();
if (rotateTransform != null) if (rotateTransform == null) return;
{
// 绕元素视觉中心旋转,避免旋转后位置乱飘 var (ox, oy, visW, visH) = GetElementVisualBox(element);
double w = element.ActualWidth > 0 ? element.ActualWidth : element.Width; double sX = scaleTransform?.ScaleX ?? 1;
double h = element.ActualHeight > 0 ? element.ActualHeight : element.Height; double sY = scaleTransform?.ScaleY ?? 1;
if (!double.IsNaN(w) && !double.IsNaN(h)) double tx = translateTransform?.X ?? 0;
{ double ty = translateTransform?.Y ?? 0;
rotateTransform.CenterX = w / 2;
rotateTransform.CenterY = h / 2; // Rotate runs last in the group, so its Center is in post-scale/translate space —
} // i.e. the current visual center of the element.
rotateTransform.Angle += angle; rotateTransform.CenterX = tx + (ox + visW / 2) * sX;
} rotateTransform.CenterY = ty + (oy + visH / 2) * sY;
rotateTransform.Angle += angle;
} }
} }
@@ -2390,6 +2393,7 @@ namespace Ink_Canvas
#region Image Selection Overlay #region Image Selection Overlay
private bool _imageOverlayHooked; private bool _imageOverlayHooked;
private FrameworkElement _overlayTrackedElement;
private void EnsureImageOverlayHooks() private void EnsureImageOverlayHooks()
{ {
@@ -2401,12 +2405,38 @@ namespace Ink_Canvas
_imageOverlayHooked = true; _imageOverlayHooked = true;
} }
private void AttachOverlayTracking(FrameworkElement element)
{
if (_overlayTrackedElement == element) return;
DetachOverlayTracking();
_overlayTrackedElement = element;
if (element != null) element.LayoutUpdated += OverlayTrackedElement_LayoutUpdated;
}
private void DetachOverlayTracking()
{
if (_overlayTrackedElement != null)
{
_overlayTrackedElement.LayoutUpdated -= OverlayTrackedElement_LayoutUpdated;
_overlayTrackedElement = null;
}
}
private void OverlayTrackedElement_LayoutUpdated(object sender, EventArgs e)
{
if (ImageSelectionOverlay?.Visibility != Visibility.Visible) return;
if (currentSelectedElement == null || _overlayTrackedElement == null) return;
if (!ReferenceEquals(currentSelectedElement, _overlayTrackedElement)) return;
UpdateImageResizeHandlesPosition(default);
}
private void ShowImageResizeHandles(FrameworkElement element) private void ShowImageResizeHandles(FrameworkElement element)
{ {
try try
{ {
if (ImageSelectionOverlay == null || element == null) return; if (ImageSelectionOverlay == null || element == null) return;
EnsureImageOverlayHooks(); EnsureImageOverlayHooks();
AttachOverlayTracking(element);
UpdateImageResizeHandlesPosition(default); UpdateImageResizeHandlesPosition(default);
ImageSelectionOverlay.Visibility = Visibility.Visible; ImageSelectionOverlay.Visibility = Visibility.Visible;
} }
@@ -2420,6 +2450,7 @@ namespace Ink_Canvas
{ {
try try
{ {
DetachOverlayTracking();
if (ImageSelectionOverlay != null) if (ImageSelectionOverlay != null)
ImageSelectionOverlay.Visibility = Visibility.Collapsed; ImageSelectionOverlay.Visibility = Visibility.Collapsed;
} }
@@ -2438,33 +2469,49 @@ namespace Ink_Canvas
if (ImageSelectionOverlay == null || currentSelectedElement == null) return; if (ImageSelectionOverlay == null || currentSelectedElement == null) return;
var element = currentSelectedElement; var element = currentSelectedElement;
double w = element.ActualWidth > 0 ? element.ActualWidth : element.Width; var (ox, oy, visW, visH) = GetElementVisualBox(element);
double h = element.ActualHeight > 0 ? element.ActualHeight : element.Height; if (visW <= 0 || visH <= 0) return;
if (double.IsNaN(w) || double.IsNaN(h) || w <= 0 || h <= 0) return;
double left = InkCanvas.GetLeft(element); double scaleX = 1, scaleY = 1, angle = 0;
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) if (element.RenderTransform is TransformGroup tg)
{ {
var st = tg.Children.OfType<ScaleTransform>().FirstOrDefault(); var st = tg.Children.OfType<ScaleTransform>().FirstOrDefault();
var tt = tg.Children.OfType<TranslateTransform>().FirstOrDefault();
var rt = tg.Children.OfType<RotateTransform>().FirstOrDefault(); var rt = tg.Children.OfType<RotateTransform>().FirstOrDefault();
if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; } if (st != null) { scaleX = st.ScaleX; scaleY = st.ScaleY; }
if (tt != null) { translateX = tt.X; translateY = tt.Y; }
if (rt != null) angle = rt.Angle; if (rt != null) angle = rt.Angle;
} }
double scaledW = w * scaleX; // Compute the visual center directly from the element's actual transform,
double scaledH = h * scaleY; // so we don't have to model the transform chain ourselves.
// Local (pre-rotation) center in canvas coordinates Point visualCenterLocal = new Point(ox + visW / 2, oy + visH / 2);
double localCenterX = left + translateX + scaledW / 2; Point centerCanvas;
double localCenterY = top + translateY + scaledH / 2; try
{
var t = element.TransformToAncestor(inkCanvas);
centerCanvas = t.Transform(visualCenterLocal);
// Cancel out rotation: TransformToAncestor includes Rotate; we need pre-rotation
// center in canvas coords for the overlay (overlay applies rotation itself).
// The visual center is invariant under rotation around itself, so this is the
// same point in canvas coords either way.
}
catch
{
double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
double tx = 0, ty = 0;
if (element.RenderTransform is TransformGroup tg2)
{
var tt = tg2.Children.OfType<TranslateTransform>().FirstOrDefault();
if (tt != null) { tx = tt.X; ty = tt.Y; }
}
centerCanvas = new Point(left + tx + (ox + visW / 2) * scaleX,
top + ty + (oy + visH / 2) * scaleY);
}
ImageSelectionOverlay.UpdateFrame(new Point(localCenterX, localCenterY), scaledW, scaledH, angle); double scaledW = visW * scaleX;
double scaledH = visH * scaleY;
ImageSelectionOverlay.UpdateFrame(centerCanvas, scaledW, scaledH, angle);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -2482,6 +2529,30 @@ namespace Ink_Canvas
return 0; return 0;
} }
// Visual box of the rendered content inside element.ActualWidth/Height,
// accounting for Stretch=Uniform letterboxing on Image elements.
// Returns (offsetX, offsetY, visibleW, visibleH) in base (unscaled) coords.
private static (double ox, double oy, double w, double h) GetElementVisualBox(FrameworkElement element)
{
double boxW = element.ActualWidth > 0 ? element.ActualWidth : element.Width;
double boxH = element.ActualHeight > 0 ? element.ActualHeight : element.Height;
if (double.IsNaN(boxW) || double.IsNaN(boxH) || boxW <= 0 || boxH <= 0)
return (0, 0, 0, 0);
if (element is Image img && img.Stretch == Stretch.Uniform && img.Source is BitmapSource bs
&& bs.PixelWidth > 0 && bs.PixelHeight > 0)
{
double srcAspect = (double)bs.PixelWidth / bs.PixelHeight;
double boxAspect = boxW / boxH;
double vW, vH;
if (srcAspect > boxAspect) { vW = boxW; vH = boxW / srcAspect; }
else { vH = boxH; vW = boxH * srcAspect; }
return ((boxW - vW) / 2, (boxH - vH) / 2, vW, vH);
}
return (0, 0, boxW, boxH);
}
// Rotate a canvas-space vector back into the element's local (unrotated) space. // Rotate a canvas-space vector back into the element's local (unrotated) space.
private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees) private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees)
{ {
@@ -2513,13 +2584,18 @@ namespace Ink_Canvas
if (currentSelectedElement.RenderTransform is TransformGroup tg) if (currentSelectedElement.RenderTransform is TransformGroup tg)
{ {
var tt = tg.Children.OfType<TranslateTransform>().FirstOrDefault(); var tt = tg.Children.OfType<TranslateTransform>().FirstOrDefault();
var rt = tg.Children.OfType<RotateTransform>().FirstOrDefault();
if (tt != null) if (tt != null)
{ {
// Translate is applied before Rotate, so convert canvas delta to local space. tt.X += e.CanvasDelta.X;
double angle = GetElementRotationAngle(currentSelectedElement); tt.Y += e.CanvasDelta.Y;
var local = CanvasVectorToLocal(e.CanvasDelta, angle); // Keep rotation center locked to the visual center so the element
tt.X += local.X; // translates rigidly instead of swinging around an old pivot.
tt.Y += local.Y; if (rt != null)
{
rt.CenterX += e.CanvasDelta.X;
rt.CenterY += e.CanvasDelta.Y;
}
} }
} }
UpdateImageResizeHandlesPosition(default); UpdateImageResizeHandlesPosition(default);
@@ -2558,21 +2634,21 @@ namespace Ink_Canvas
if (scaleTransform == null || translateTransform == null) return; if (scaleTransform == null || translateTransform == null) return;
double angle = rotateTransform?.Angle ?? 0; double angle = rotateTransform?.Angle ?? 0;
double baseW = element.ActualWidth > 0 ? element.ActualWidth : element.Width;
double baseH = element.ActualHeight > 0 ? element.ActualHeight : element.Height; var (ox, oy, visW, visH) = GetElementVisualBox(element);
if (double.IsNaN(baseW) || double.IsNaN(baseH) || baseW <= 0 || baseH <= 0) return; if (visW <= 0 || visH <= 0) return;
double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0; double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0; double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
double curW = baseW * scaleTransform.ScaleX; double curW = visW * scaleTransform.ScaleX;
double curH = baseH * scaleTransform.ScaleY; double curH = visH * scaleTransform.ScaleY;
// Drag delta → local (unrotated) space so corner tracks cursor under any rotation. // Drag delta → local (unrotated) space so corner tracks cursor under any rotation.
Vector local = CanvasVectorToLocal(canvasDelta, angle); Vector local = CanvasVectorToLocal(canvasDelta, angle);
double newW = curW, newH = curH; double newW = curW, newH = curH;
double pivotFracX = 0, pivotFracY = 0; // opposite corner as fraction of scaled box double pivotFracX = 0, pivotFracY = 0; // opposite visual corner
switch (corner) switch (corner)
{ {
case ImageResizeCorner.TopLeft: case ImageResizeCorner.TopLeft:
@@ -2600,56 +2676,72 @@ namespace Ink_Canvas
newH = curH * uniform; newH = curH * uniform;
} }
// Clamp final scale double newScaleX = newW / visW;
double newScaleX = newW / baseW; double newScaleY = newH / visH;
double newScaleY = newH / baseH;
newScaleX = Math.Max(0.1, Math.Min(newScaleX, 10.0)); newScaleX = Math.Max(0.1, Math.Min(newScaleX, 10.0));
newScaleY = Math.Max(0.1, Math.Min(newScaleY, 10.0)); newScaleY = Math.Max(0.1, Math.Min(newScaleY, 10.0));
newW = baseW * newScaleX; newW = visW * newScaleX;
newH = baseH * newScaleY; newH = visH * newScaleY;
double tx = translateTransform.X; double tx = translateTransform.X;
double ty = translateTransform.Y; double ty = translateTransform.Y;
// Pivot canvas position BEFORE the change // Visual pivot canvas position BEFORE the change.
Point pivotBefore = ComputeCornerCanvas(pivotFracX * curW, pivotFracY * curH, // Visual box origin (pre-scale) is (ox, oy); after scale its top-left in post-scale
curW, curH, left, top, tx, ty, angle); // space (before rotation/translate) is (ox*sx, oy*sy), size (visW*sx, visH*sy).
// Pivot canvas position AFTER the scale change, assuming translate unchanged // Pivot pre-rotation = (tx + (ox + pivotFrac*visW) * sx, ty + (oy + pivotFrac*visH) * sy).
Point pivotAfter = ComputeCornerCanvas(pivotFracX * newW, pivotFracY * newH, // Rotation center is the element visual center, which is the SAME pre-rotation anchor
newW, newH, left, top, tx, ty, angle); // we derive below — so pre-rotation coord == canvas coord relative to (left, top) up
// to a rotation around that center. Because we rotate around the center and both
// before/after share the same center (we'll update it to newCenter below after shift),
// we must match canvas positions WITH rotation applied.
Point pivotBefore = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
ox + visW / 2, oy + visH / 2,
scaleTransform.ScaleX, scaleTransform.ScaleY,
tx, ty, angle, left, top);
Point pivotAfter = ApplyCanvasTransform(ox + pivotFracX * visW, oy + pivotFracY * visH,
ox + visW / 2, oy + visH / 2,
newScaleX, newScaleY,
tx, ty, angle, left, top);
// Shift translate in local space to bring pivotAfter back to pivotBefore
Vector canvasDrift = pivotBefore - pivotAfter; Vector canvasDrift = pivotBefore - pivotAfter;
Vector localShift = CanvasVectorToLocal(canvasDrift, angle); translateTransform.X += canvasDrift.X;
translateTransform.X += localShift.X; translateTransform.Y += canvasDrift.Y;
translateTransform.Y += localShift.Y;
scaleTransform.ScaleX = newScaleX; scaleTransform.ScaleX = newScaleX;
scaleTransform.ScaleY = newScaleY; scaleTransform.ScaleY = newScaleY;
// Update rotation center to the new visual center so future rotations behave.
if (rotateTransform != null)
{
rotateTransform.CenterX = translateTransform.X + (ox + visW / 2) * newScaleX;
rotateTransform.CenterY = translateTransform.Y + (oy + visH / 2) * newScaleY;
}
UpdateImageResizeHandlesPosition(default); UpdateImageResizeHandlesPosition(default);
if (BorderImageSelectionControl?.Visibility == Visibility.Visible) if (BorderImageSelectionControl?.Visibility == Visibility.Visible)
UpdateImageSelectionToolbarPosition(element); UpdateImageSelectionToolbarPosition(element);
} }
// Canvas position of local point (lx, ly) inside the scaled box of size (sW, sH), // Canvas position of element-local point (lx, ly) under the given transform.
// given element placement (left, top), translate (tx, ty) and rotation angle (deg). // Model: P_canvas = (left, top) + Rotate_center(S * (lx,ly) + T)
// Rotation pivot is the visual center: pre-rotate (tx + sW/2, ty + sH/2). // where center = S * (cx, cy) + T, and rotation angle is angleDeg.
private static Point ComputeCornerCanvas(double lx, double ly, double sW, double sH, private static Point ApplyCanvasTransform(double lx, double ly, double cx, double cy,
double left, double top, double tx, double ty, double sx, double sy, double tx, double ty,
double angleDeg) double angleDeg, double left, double top)
{ {
double preX = tx + lx; double preX = sx * lx + tx;
double preY = ty + ly; double preY = sy * ly + ty;
double cx = tx + sW / 2; double centerX = sx * cx + tx;
double cy = ty + sH / 2; double centerY = sy * cy + ty;
double rad = angleDeg * Math.PI / 180.0; double rad = angleDeg * Math.PI / 180.0;
double cos = Math.Cos(rad); double cos = Math.Cos(rad);
double sin = Math.Sin(rad); double sin = Math.Sin(rad);
double relX = preX - cx; double relX = preX - centerX;
double relY = preY - cy; double relY = preY - centerY;
double rotX = relX * cos - relY * sin + cx; double rotX = relX * cos - relY * sin + centerX;
double rotY = relX * sin + relY * cos + cy; double rotY = relX * sin + relY * cos + centerY;
return new Point(left + rotX, top + rotY); return new Point(left + rotX, top + rotY);
} }