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)
{
var scaleTransform = transformGroup.Children.OfType<ScaleTransform>().FirstOrDefault();
var translateTransform = transformGroup.Children.OfType<TranslateTransform>().FirstOrDefault();
var rotateTransform = transformGroup.Children.OfType<RotateTransform>().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;
}
if (rotateTransform == null) return;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
double sX = scaleTransform?.ScaleX ?? 1;
double sY = scaleTransform?.ScaleY ?? 1;
double tx = translateTransform?.X ?? 0;
double ty = translateTransform?.Y ?? 0;
// 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.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
private bool _imageOverlayHooked;
private FrameworkElement _overlayTrackedElement;
private void EnsureImageOverlayHooks()
{
@@ -2401,12 +2405,38 @@ namespace Ink_Canvas
_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)
{
try
{
if (ImageSelectionOverlay == null || element == null) return;
EnsureImageOverlayHooks();
AttachOverlayTracking(element);
UpdateImageResizeHandlesPosition(default);
ImageSelectionOverlay.Visibility = Visibility.Visible;
}
@@ -2420,6 +2450,7 @@ namespace Ink_Canvas
{
try
{
DetachOverlayTracking();
if (ImageSelectionOverlay != null)
ImageSelectionOverlay.Visibility = Visibility.Collapsed;
}
@@ -2438,33 +2469,49 @@ namespace Ink_Canvas
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;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
if (visW <= 0 || visH <= 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;
double scaleX = 1, scaleY = 1, angle = 0;
if (element.RenderTransform is TransformGroup tg)
{
var st = tg.Children.OfType<ScaleTransform>().FirstOrDefault();
var tt = tg.Children.OfType<TranslateTransform>().FirstOrDefault();
var rt = tg.Children.OfType<RotateTransform>().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;
// Compute the visual center directly from the element's actual transform,
// so we don't have to model the transform chain ourselves.
Point visualCenterLocal = new Point(ox + visW / 2, oy + visH / 2);
Point centerCanvas;
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)
{
@@ -2482,6 +2529,30 @@ namespace Ink_Canvas
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.
private Vector CanvasVectorToLocal(Vector canvasDelta, double angleDegrees)
{
@@ -2513,13 +2584,18 @@ namespace Ink_Canvas
if (currentSelectedElement.RenderTransform is TransformGroup tg)
{
var tt = tg.Children.OfType<TranslateTransform>().FirstOrDefault();
var rt = tg.Children.OfType<RotateTransform>().FirstOrDefault();
if (tt != null)
{
// 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;
tt.X += e.CanvasDelta.X;
tt.Y += e.CanvasDelta.Y;
// Keep rotation center locked to the visual center so the element
// translates rigidly instead of swinging around an old pivot.
if (rt != null)
{
rt.CenterX += e.CanvasDelta.X;
rt.CenterY += e.CanvasDelta.Y;
}
}
}
UpdateImageResizeHandlesPosition(default);
@@ -2558,21 +2634,21 @@ namespace Ink_Canvas
if (scaleTransform == null || translateTransform == null) 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;
var (ox, oy, visW, visH) = GetElementVisualBox(element);
if (visW <= 0 || visH <= 0) return;
double left = InkCanvas.GetLeft(element); if (double.IsNaN(left)) left = 0;
double top = InkCanvas.GetTop(element); if (double.IsNaN(top)) top = 0;
double curW = baseW * scaleTransform.ScaleX;
double curH = baseH * scaleTransform.ScaleY;
double curW = visW * scaleTransform.ScaleX;
double curH = visH * 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
double pivotFracX = 0, pivotFracY = 0; // opposite visual corner
switch (corner)
{
case ImageResizeCorner.TopLeft:
@@ -2600,56 +2676,72 @@ namespace Ink_Canvas
newH = curH * uniform;
}
// Clamp final scale
double newScaleX = newW / baseW;
double newScaleY = newH / baseH;
double newScaleX = newW / visW;
double newScaleY = newH / visH;
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;
newW = visW * newScaleX;
newH = visH * newScaleY;
double tx = translateTransform.X;
double ty = translateTransform.Y;
// 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);
// Visual pivot canvas position BEFORE the change.
// Visual box origin (pre-scale) is (ox, oy); after scale its top-left in post-scale
// space (before rotation/translate) is (ox*sx, oy*sy), size (visW*sx, visH*sy).
// Pivot pre-rotation = (tx + (ox + pivotFrac*visW) * sx, ty + (oy + pivotFrac*visH) * sy).
// Rotation center is the element visual center, which is the SAME pre-rotation anchor
// 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 localShift = CanvasVectorToLocal(canvasDrift, angle);
translateTransform.X += localShift.X;
translateTransform.Y += localShift.Y;
translateTransform.X += canvasDrift.X;
translateTransform.Y += canvasDrift.Y;
scaleTransform.ScaleX = newScaleX;
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);
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)
// Canvas position of element-local point (lx, ly) under the given transform.
// Model: P_canvas = (left, top) + Rotate_center(S * (lx,ly) + T)
// where center = S * (cx, cy) + T, and rotation angle is angleDeg.
private static Point ApplyCanvasTransform(double lx, double ly, double cx, double cy,
double sx, double sy, double tx, double ty,
double angleDeg, double left, double top)
{
double preX = tx + lx;
double preY = ty + ly;
double cx = tx + sW / 2;
double cy = ty + sH / 2;
double preX = sx * lx + tx;
double preY = sy * ly + ty;
double centerX = sx * cx + tx;
double centerY = sy * cy + ty;
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;
double relX = preX - centerX;
double relY = preY - centerY;
double rotX = relX * cos - relY * sin + centerX;
double rotY = relX * sin + relY * cos + centerY;
return new Point(left + rotX, top + rotY);
}