namespace AmiReel.Models; /// Edge insets, in source pixels, removed by a manual crop. public readonly record struct PixelInsets(int Left, int Right, int Top, int Bottom) { public bool IsEmpty => Left == 0 && Right == 0 && Top == 0 && Bottom == 0; } /// Which part of an existing crop rectangle a pointer is on. public enum CropHit { None, Move, North, South, East, West, NorthWest, NorthEast, SouthWest, SouthEast, } /// Maps a dragged display rectangle onto source-pixel crop insets. public static class SourceCropSelection { public const int MinimumSize = 16; public static (double OffsetX, double OffsetY, double DisplayWidth, double DisplayHeight) FitUniform( double containerWidth, double containerHeight, int pixelWidth, int pixelHeight) { if (containerWidth <= 0 || containerHeight <= 0 || pixelWidth <= 0 || pixelHeight <= 0) return (0, 0, 0, 0); double containerAspect = containerWidth / containerHeight; double imageAspect = pixelWidth / (double)pixelHeight; double displayWidth; double displayHeight; if (containerAspect > imageAspect) { displayHeight = containerHeight; displayWidth = containerHeight * imageAspect; } else { displayWidth = containerWidth; displayHeight = containerWidth / imageAspect; } return ((containerWidth - displayWidth) / 2, (containerHeight - displayHeight) / 2, displayWidth, displayHeight); } public static PixelInsets ToInsets( int pixelWidth, int pixelHeight, double displayWidth, double displayHeight, double x, double y, double width, double height) { if (pixelWidth <= 0 || pixelHeight <= 0 || displayWidth <= 0 || displayHeight <= 0) return default; Normalize(ref x, ref y, ref width, ref height); x = Math.Clamp(x, 0, displayWidth); y = Math.Clamp(y, 0, displayHeight); width = Math.Clamp(width, 0, displayWidth - x); height = Math.Clamp(height, 0, displayHeight - y); double scaleX = pixelWidth / displayWidth; double scaleY = pixelHeight / displayHeight; int left = ToEvenPixel(x * scaleX, pixelWidth - MinimumSize); int top = ToEvenPixel(y * scaleY, pixelHeight - MinimumSize); int cropWidth = ToEvenPixel(width * scaleX, pixelWidth - left); int cropHeight = ToEvenPixel(height * scaleY, pixelHeight - top); if (cropWidth < MinimumSize) cropWidth = Math.Min(MinimumSize, pixelWidth - left); if (cropHeight < MinimumSize) cropHeight = Math.Min(MinimumSize, pixelHeight - top); return new PixelInsets(left, pixelWidth - left - cropWidth, top, pixelHeight - top - cropHeight); } public static (double X, double Y, double Width, double Height) ToDisplayRect( int pixelWidth, int pixelHeight, double displayWidth, double displayHeight, PixelInsets insets) { if (pixelWidth <= 0 || pixelHeight <= 0 || displayWidth <= 0 || displayHeight <= 0) return (0, 0, Math.Max(0, displayWidth), Math.Max(0, displayHeight)); double scaleX = displayWidth / pixelWidth; double scaleY = displayHeight / pixelHeight; double x = insets.Left * scaleX; double y = insets.Top * scaleY; double width = Math.Max(1, (pixelWidth - insets.Left - insets.Right) * scaleX); double height = Math.Max(1, (pixelHeight - insets.Top - insets.Bottom) * scaleY); return (x, y, width, height); } public static (double X, double Y) ClampToDisplay(double x, double y, double displayWidth, double displayHeight) => (Math.Clamp(x, 0, Math.Max(0, displayWidth)), Math.Clamp(y, 0, Math.Max(0, displayHeight))); public static CropHit HitTest( double rectX, double rectY, double rectWidth, double rectHeight, double pointerX, double pointerY, double handleSize) { if (rectWidth <= 0 || rectHeight <= 0 || handleSize < 0) return CropHit.None; bool nearLeft = Math.Abs(pointerX - rectX) <= handleSize; bool nearRight = Math.Abs(pointerX - (rectX + rectWidth)) <= handleSize; bool nearTop = Math.Abs(pointerY - rectY) <= handleSize; bool nearBottom = Math.Abs(pointerY - (rectY + rectHeight)) <= handleSize; bool alongX = pointerX >= rectX - handleSize && pointerX <= rectX + rectWidth + handleSize; bool alongY = pointerY >= rectY - handleSize && pointerY <= rectY + rectHeight + handleSize; if (nearTop && nearLeft) return CropHit.NorthWest; if (nearTop && nearRight) return CropHit.NorthEast; if (nearBottom && nearLeft) return CropHit.SouthWest; if (nearBottom && nearRight) return CropHit.SouthEast; if (nearTop && alongX) return CropHit.North; if (nearBottom && alongX) return CropHit.South; if (nearLeft && alongY) return CropHit.West; if (nearRight && alongY) return CropHit.East; if (pointerX >= rectX && pointerX <= rectX + rectWidth && pointerY >= rectY && pointerY <= rectY + rectHeight) return CropHit.Move; return CropHit.None; } public static (double X, double Y, double Width, double Height) AdjustRect( double x, double y, double width, double height, CropHit hit, double deltaX, double deltaY, double displayWidth, double displayHeight) { const double minSize = 8; double left = x; double top = y; double right = x + width; double bottom = y + height; if (hit == CropHit.Move) { left = Math.Clamp(left + deltaX, 0, Math.Max(0, displayWidth - width)); top = Math.Clamp(top + deltaY, 0, Math.Max(0, displayHeight - height)); return (left, top, width, height); } if (hit is CropHit.West or CropHit.NorthWest or CropHit.SouthWest) left = Math.Clamp(left + deltaX, 0, right - minSize); if (hit is CropHit.East or CropHit.NorthEast or CropHit.SouthEast) right = Math.Clamp(right + deltaX, left + minSize, displayWidth); if (hit is CropHit.North or CropHit.NorthWest or CropHit.NorthEast) top = Math.Clamp(top + deltaY, 0, bottom - minSize); if (hit is CropHit.South or CropHit.SouthWest or CropHit.SouthEast) bottom = Math.Clamp(bottom + deltaY, top + minSize, displayHeight); return (left, top, Math.Max(minSize, right - left), Math.Max(minSize, bottom - top)); } public static PixelInsets Nudge(PixelInsets insets, int pixelWidth, int pixelHeight, int deltaX, int deltaY) { int width = pixelWidth - insets.Left - insets.Right; int height = pixelHeight - insets.Top - insets.Bottom; if (width < MinimumSize || height < MinimumSize) return insets; int left = ToEvenPixel(insets.Left + deltaX, pixelWidth - width); int top = ToEvenPixel(insets.Top + deltaY, pixelHeight - height); return new PixelInsets(left, pixelWidth - left - width, top, pixelHeight - top - height); } public static PixelInsets NudgeEdge( PixelInsets insets, int pixelWidth, int pixelHeight, int dLeft, int dTop, int dRight, int dBottom) { int left = ToEvenPixel(insets.Left + dLeft, pixelWidth - MinimumSize); int top = ToEvenPixel(insets.Top + dTop, pixelHeight - MinimumSize); int right = ToEvenPixel(insets.Right + dRight, pixelWidth - MinimumSize - left); int bottom = ToEvenPixel(insets.Bottom + dBottom, pixelHeight - MinimumSize - top); if (pixelWidth - left - right < MinimumSize) right = Math.Max(0, pixelWidth - left - MinimumSize); if (pixelHeight - top - bottom < MinimumSize) bottom = Math.Max(0, pixelHeight - top - MinimumSize); return new PixelInsets(left, right, top, bottom); } private static void Normalize(ref double x, ref double y, ref double width, ref double height) { if (width < 0) { x += width; width = -width; } if (height < 0) { y += height; height = -height; } } private static int ToEvenPixel(double value, int max) { int rounded = (int)Math.Round(value, MidpointRounding.AwayFromZero); int clamped = Math.Clamp(rounded, 0, Math.Max(0, max)); return (clamped / 2) * 2; } }