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; } /// 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 right = ToEvenPixel((displayWidth - x - width) * scaleX, pixelWidth - MinimumSize - left); int bottom = ToEvenPixel((displayHeight - y - height) * scaleY, 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); } 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))); 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; } }