diff --git a/src/SixLabors.Core/Constants.cs b/src/SixLabors.Core/Constants.cs new file mode 100644 index 0000000000..b8699e2d15 --- /dev/null +++ b/src/SixLabors.Core/Constants.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors +{ + /// + /// Common constants used throughout the project. + /// + internal static class Constants + { + /// + /// The epsilon for comparing floating point numbers. + /// + public static readonly float Epsilon = 0.001f; + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/GeometryUtilities.cs b/src/SixLabors.Core/GeometryUtilities.cs new file mode 100644 index 0000000000..43c46f1812 --- /dev/null +++ b/src/SixLabors.Core/GeometryUtilities.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors +{ + /// + /// Utility class for common geometric functions. + /// + public static class GeometryUtilities + { + /// + /// Converts a degree (360-periodic) angle to a radian (2*Pi-periodic) angle. + /// + /// The angle in degrees. + /// + /// The representing the degree as radians. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DegreeToRadian(float degree) => degree * (MathF.PI / 180F); + + /// + /// Converts a radian (2*Pi-periodic) angle to a degree (360-periodic) angle. + /// + /// The angle in radians. + /// + /// The representing the degree as radians. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float RadianToDegree(float radian) => radian / (MathF.PI / 180F); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/AllocationOptions.cs b/src/SixLabors.Core/Memory/AllocationOptions.cs new file mode 100644 index 0000000000..5eda00505e --- /dev/null +++ b/src/SixLabors.Core/Memory/AllocationOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory +{ + /// + /// Options for allocating buffers. + /// + public enum AllocationOptions + { + /// + /// Indicates that the buffer should just be allocated. + /// + None, + + /// + /// Indicates that the allocated buffer should be cleaned following allocation. + /// + Clean + } +} diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs new file mode 100644 index 0000000000..5676ab23fc --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.Memory.Internals; + +namespace SixLabors.Memory +{ + /// + /// Contains and . + /// + public partial class ArrayPoolMemoryAllocator + { + /// + /// The buffer implementation of . + /// + private class Buffer : ManagedBufferBase + where T : struct + { + /// + /// The length of the buffer. + /// + private readonly int length; + + /// + /// A weak reference to the source pool. + /// + /// + /// By using a weak reference here, we are making sure that array pools and their retained arrays are always GC-ed + /// after a call to , regardless of having buffer instances still being in use. + /// + private WeakReference> sourcePoolReference; + + public Buffer(byte[] data, int length, ArrayPool sourcePool) + { + this.Data = data; + this.length = length; + this.sourcePoolReference = new WeakReference>(sourcePool); + } + + /// + /// Gets the buffer as a byte array. + /// + protected byte[] Data { get; private set; } + + /// + public override Span GetSpan() => MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); + + /// + protected override void Dispose(bool disposing) + { + if (!disposing || this.Data is null || this.sourcePoolReference is null) + { + return; + } + + if (this.sourcePoolReference.TryGetTarget(out ArrayPool pool)) + { + pool.Return(this.Data); + } + + this.sourcePoolReference = null; + this.Data = null; + } + + protected override object GetPinnableObject() => this.Data; + } + + /// + /// The implementation of . + /// + private sealed class ManagedByteBuffer : Buffer, IManagedByteBuffer + { + public ManagedByteBuffer(byte[] data, int length, ArrayPool sourcePool) + : base(data, length, sourcePool) + { + } + + /// + public byte[] Array => this.Data; + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs new file mode 100644 index 0000000000..dd6e9a8f0b --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory +{ + /// + /// Contains common factory methods and configuration constants. + /// + public partial class ArrayPoolMemoryAllocator + { + /// + /// The default value for: maximum size of pooled arrays in bytes. + /// Currently set to 24MB, which is equivalent to 8 megapixels of raw RGBA32 data. + /// + internal const int DefaultMaxPooledBufferSizeInBytes = 24 * 1024 * 1024; + + /// + /// The value for: The threshold to pool arrays in which has less buckets for memory safety. + /// + private const int DefaultBufferSelectorThresholdInBytes = 8 * 1024 * 1024; + + /// + /// The default bucket count for . + /// + private const int DefaultLargePoolBucketCount = 6; + + /// + /// The default bucket count for . + /// + private const int DefaultNormalPoolBucketCount = 16; + + /// + /// This is the default. Should be good for most use cases. + /// + /// The memory manager. + public static ArrayPoolMemoryAllocator CreateDefault() + { + return new ArrayPoolMemoryAllocator( + DefaultMaxPooledBufferSizeInBytes, + DefaultBufferSelectorThresholdInBytes, + DefaultLargePoolBucketCount, + DefaultNormalPoolBucketCount); + } + + /// + /// For environments with very limited memory capabilities, only small buffers like image rows are pooled. + /// + /// The memory manager. + public static ArrayPoolMemoryAllocator CreateWithMinimalPooling() + { + return new ArrayPoolMemoryAllocator(64 * 1024, 32 * 1024, 8, 24); + } + + /// + /// For environments with limited memory capabilities, only small array requests are pooled, which can result in reduced throughput. + /// + /// The memory manager. + public static ArrayPoolMemoryAllocator CreateWithModeratePooling() + { + return new ArrayPoolMemoryAllocator(1024 * 1024, 32 * 1024, 16, 24); + } + + /// + /// For environments where memory capabilities are not an issue, the maximum amount of array requests are pooled which results in optimal throughput. + /// + /// The memory manager. + public static ArrayPoolMemoryAllocator CreateWithAggressivePooling() + { + return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs new file mode 100644 index 0000000000..0905948e00 --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace SixLabors.Memory +{ + /// + /// Implements by allocating memory from . + /// + public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator + { + private readonly int maxArraysPerBucketNormalPool; + + private readonly int maxArraysPerBucketLargePool; + + /// + /// The for small-to-medium buffers which is not kept clean. + /// + private ArrayPool normalArrayPool; + + /// + /// The for huge buffers, which is not kept clean. + /// + private ArrayPool largeArrayPool; + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolMemoryAllocator() + : this(DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes) + : this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// Arrays over this threshold will be pooled in which has less buckets for memory safety. + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes) + : this(maxPoolSizeInBytes, poolSelectorThresholdInBytes, DefaultLargePoolBucketCount, DefaultNormalPoolBucketCount) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// The threshold to pool arrays in which has less buckets for memory safety. + /// Max arrays per bucket for the large array pool. + /// Max arrays per bucket for the normal array pool. + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes, int maxArraysPerBucketLargePool, int maxArraysPerBucketNormalPool) + { + Guard.MustBeGreaterThan(maxPoolSizeInBytes, 0, nameof(maxPoolSizeInBytes)); + Guard.MustBeLessThanOrEqualTo(poolSelectorThresholdInBytes, maxPoolSizeInBytes, nameof(poolSelectorThresholdInBytes)); + + this.MaxPoolSizeInBytes = maxPoolSizeInBytes; + this.PoolSelectorThresholdInBytes = poolSelectorThresholdInBytes; + this.maxArraysPerBucketLargePool = maxArraysPerBucketLargePool; + this.maxArraysPerBucketNormalPool = maxArraysPerBucketNormalPool; + + this.InitArrayPools(); + } + + /// + /// Gets the maximum size of pooled arrays in bytes. + /// + public int MaxPoolSizeInBytes { get; } + + /// + /// Gets the threshold to pool arrays in which has less buckets for memory safety. + /// + public int PoolSelectorThresholdInBytes { get; } + + /// + public override void ReleaseRetainedResources() + { + this.InitArrayPools(); + } + + /// + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + int itemSizeBytes = Unsafe.SizeOf(); + int bufferSizeInBytes = length * itemSizeBytes; + if (bufferSizeInBytes < 0) + { + throw new ArgumentOutOfRangeException( + nameof(length), + $"{nameof(ArrayPoolMemoryAllocator)} can not allocate {length} elements of {typeof(T).Name}."); + } + + ArrayPool pool = this.GetArrayPool(bufferSizeInBytes); + byte[] byteArray = pool.Rent(bufferSizeInBytes); + + var buffer = new Buffer(byteArray, length, pool); + if (options == AllocationOptions.Clean) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + /// + public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + + ArrayPool pool = this.GetArrayPool(length); + byte[] byteArray = pool.Rent(length); + + var buffer = new ManagedByteBuffer(byteArray, length, pool); + if (options == AllocationOptions.Clean) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + private static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes) + { + return maxPoolSizeInBytes / 4; + } + + private ArrayPool GetArrayPool(int bufferSizeInBytes) + { + return bufferSizeInBytes <= this.PoolSelectorThresholdInBytes ? this.normalArrayPool : this.largeArrayPool; + } + + private void InitArrayPools() + { + this.largeArrayPool = ArrayPool.Create(this.MaxPoolSizeInBytes, this.maxArraysPerBucketLargePool); + this.normalArrayPool = ArrayPool.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/IManagedByteBuffer.cs b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs new file mode 100644 index 0000000000..b6d956c102 --- /dev/null +++ b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.Memory +{ + /// + /// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s. + /// + public interface IManagedByteBuffer : IMemoryOwner + { + /// + /// Gets the managed array backing this buffer instance. + /// + byte[] Array { get; } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs b/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs new file mode 100644 index 0000000000..c0d36afd55 --- /dev/null +++ b/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.Memory.Internals +{ + /// + /// Wraps an array as an instance. + /// + /// + internal class BasicArrayBuffer : ManagedBufferBase + where T : struct + { + /// + /// Initializes a new instance of the class. + /// + /// The array. + /// The length of the buffer. + public BasicArrayBuffer(T[] array, int length) + { + DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); + this.Array = array; + this.Length = length; + } + + /// + /// Initializes a new instance of the class. + /// + /// The array. + public BasicArrayBuffer(T[] array) + : this(array, array.Length) + { + } + + /// + /// Gets the array. + /// + public T[] Array { get; } + + /// + /// Gets the length. + /// + public int Length { get; } + + /// + public override Span GetSpan() => this.Array.AsSpan(0, this.Length); + + /// + protected override void Dispose(bool disposing) + { + } + + /// + protected override object GetPinnableObject() + { + return this.Array; + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs b/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs new file mode 100644 index 0000000000..fa6a5de4c7 --- /dev/null +++ b/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory.Internals +{ + /// + /// Provides an based on . + /// + internal sealed class BasicByteBuffer : BasicArrayBuffer, IManagedByteBuffer + { + /// + /// Initializes a new instance of the class. + /// + /// The byte array. + internal BasicByteBuffer(byte[] array) + : base(array) + { + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs b/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs new file mode 100644 index 0000000000..e1f131693c --- /dev/null +++ b/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Runtime.InteropServices; + +namespace SixLabors.Memory.Internals +{ + /// + /// Provides a base class for implementations by implementing pinning logic for adaption. + /// + /// The element type. + internal abstract class ManagedBufferBase : MemoryManager + where T : struct + { + private GCHandle pinHandle; + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if (!this.pinHandle.IsAllocated) + { + this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned); + } + + void* ptr = (void*)this.pinHandle.AddrOfPinnedObject(); + return new MemoryHandle(ptr, this.pinHandle); + } + + /// + public override void Unpin() + { + if (this.pinHandle.IsAllocated) + { + this.pinHandle.Free(); + } + } + + /// + /// Gets the object that should be pinned. + /// + /// The pinnable . + protected abstract object GetPinnableObject(); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/MemoryAllocator.cs b/src/SixLabors.Core/Memory/MemoryAllocator.cs new file mode 100644 index 0000000000..24ed7bef38 --- /dev/null +++ b/src/SixLabors.Core/Memory/MemoryAllocator.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.Memory +{ + /// + /// Memory managers are used to allocate memory for image processing operations. + /// + public abstract class MemoryAllocator + { + /// + /// Allocates an , holding a of length . + /// + /// Type of the data stored in the buffer. + /// Size of the buffer to allocate. + /// The allocation options. + /// A buffer of values of type . + public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + where T : struct; + + /// + /// Allocates an . + /// + /// The requested buffer length. + /// The allocation options. + /// The . + public abstract IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None); + + /// + /// Releases all retained resources not being in use. + /// Eg: by resetting array pools and letting GC to free the arrays. + /// + public virtual void ReleaseRetainedResources() + { + } + } +} diff --git a/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs new file mode 100644 index 0000000000..acf17ad63d --- /dev/null +++ b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.Memory.Internals; + +namespace SixLabors.Memory +{ + /// + /// Implements by newing up arrays by the GC on every allocation requests. + /// + public sealed class SimpleGcMemoryAllocator : MemoryAllocator + { + /// + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + + return new BasicArrayBuffer(new T[length]); + } + + /// + public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + + return new BasicByteBuffer(new byte[length]); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs b/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs new file mode 100644 index 0000000000..2d33ea70d6 --- /dev/null +++ b/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; + +namespace SixLabors.Primitives +{ + /// + /// Extension methods for the struct. + /// + public static class Matrix3x2Extensions + { + /// + /// Creates a translation matrix from the given vector. + /// + /// The translation position. + /// A translation matrix. + public static Matrix3x2 CreateTranslation(PointF position) => Matrix3x2.CreateTranslation(position); + + /// + /// Creates a scale matrix that is offset by a given center point. + /// + /// Value to scale by on the X-axis. + /// Value to scale by on the Y-axis. + /// The center point. + /// A scaling matrix. + public static Matrix3x2 CreateScale(float xScale, float yScale, PointF centerPoint) => Matrix3x2.CreateScale(xScale, yScale, centerPoint); + + /// + /// Creates a scale matrix from the given vector scale. + /// + /// The scale to use. + /// A scaling matrix. + public static Matrix3x2 CreateScale(SizeF scales) => Matrix3x2.CreateScale(scales); + + /// + /// Creates a scale matrix from the given vector scale with an offset from the given center point. + /// + /// The scale to use. + /// The center offset. + /// A scaling matrix. + public static Matrix3x2 CreateScale(SizeF scales, PointF centerPoint) => Matrix3x2.CreateScale(scales, centerPoint); + + /// + /// Creates a scale matrix that scales uniformly with the given scale with an offset from the given center. + /// + /// The uniform scale to use. + /// The center offset. + /// A scaling matrix. + public static Matrix3x2 CreateScale(float scale, PointF centerPoint) => Matrix3x2.CreateScale(scale, centerPoint); + + /// + /// Creates a skew matrix from the given angles in degrees. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// A skew matrix. + public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY)); + + /// + /// Creates a skew matrix from the given angles in radians and a center point. + /// + /// The X angle, in radians. + /// The Y angle, in radians. + /// The center point. + /// A skew matrix. + public static Matrix3x2 CreateSkew(float radiansX, float radiansY, PointF centerPoint) => Matrix3x2.CreateSkew(radiansX, radiansY, centerPoint); + + /// + /// Creates a skew matrix from the given angles in degrees and a center point. + /// + /// The X angle, in degrees. + /// The Y angle, in degrees. + /// The center point. + /// A skew matrix. + public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY, PointF centerPoint) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint); + + /// + /// Creates a rotation matrix using the given rotation in degrees. + /// + /// The amount of rotation, in degrees. + /// A rotation matrix. + public static Matrix3x2 CreateRotationDegrees(float degrees) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees)); + + /// + /// Creates a rotation matrix using the given rotation in radians and a center point. + /// + /// The amount of rotation, in radians. + /// The center point. + /// A rotation matrix. + public static Matrix3x2 CreateRotation(float radians, PointF centerPoint) => Matrix3x2.CreateRotation(radians, centerPoint); + + /// + /// Creates a rotation matrix using the given rotation in degrees and a center point. + /// + /// The amount of rotation, in degrees. + /// The center point. + /// A rotation matrix. + public static Matrix3x2 CreateRotationDegrees(float degrees, PointF centerPoint) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/Point.cs b/src/SixLabors.Core/Primitives/Point.cs new file mode 100644 index 0000000000..ec57330411 --- /dev/null +++ b/src/SixLabors.Core/Primitives/Point.cs @@ -0,0 +1,288 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Represents an ordered pair of integer x- and y-coordinates that defines a point in + /// a two-dimensional plane. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Point : IEquatable + { + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Point Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal and vertical position of the point. + public Point(int value) + : this() + { + this.X = LowInt16(value); + this.Y = HighInt16(value); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the point. + /// The vertical position of the point. + public Point(int x, int y) + : this() + { + this.X = x; + this.Y = y; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The size. + public Point(Size size) + { + this.X = size.Width; + this.Y = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public int Y { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator PointF(Point point) => new PointF(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(Point point) => new Vector2(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Size(Point point) => new Size(point.X, point.Y); + + /// + /// Negates the given point by multiplying all values by -1. + /// + /// The source point. + /// The negated point. + public static Point operator -(Point value) => new Point(-value.X, -value.Y); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator +(Point point, Size size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point operator -(Point point, Size size) => Subtract(point, size); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static Point operator *(int left, Point right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static Point operator *(Point left, int right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static Point operator /(Point left, int right) + => new Point(left.X / right, left.Y / right); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Point left, Point right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Point left, Point right) => !left.Equals(right); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Add(Point point, Size size) => new Point(unchecked(point.X + size.Width), unchecked(point.Y + size.Height)); + + /// + /// Translates a by the negative of a given value. + /// + /// The point on the left hand of the operand. + /// The value on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Multiply(Point point, int value) => new Point(unchecked(point.X * value), unchecked(point.Y * value)); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Subtract(Point point, Size size) => new Point(unchecked(point.X - size.Width), unchecked(point.Y - size.Height)); + + /// + /// Converts a to a by performing a ceiling operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Ceiling(PointF point) => new Point(unchecked((int)MathF.Ceiling(point.X)), unchecked((int)MathF.Ceiling(point.Y))); + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Round(PointF point) => new Point(unchecked((int)MathF.Round(point.X)), unchecked((int)MathF.Round(point.Y))); + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Round(Vector2 vector) => new Point(unchecked((int)MathF.Round(vector.X)), unchecked((int)MathF.Round(vector.Y))); + + /// + /// Converts a to a by performing a truncate operation on all the coordinates. + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Truncate(PointF point) => new Point(unchecked((int)point.X), unchecked((int)point.Y)); + + /// + /// Transforms a point by a specified 3x2 matrix. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix)); + + /// + /// Deconstructs this point into two integers. + /// + /// The out value for X. + /// The out value for Y. + public void Deconstruct(out int x, out int y) + { + x = this.X; + y = this.Y; + } + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(int dx, int dy) + { + unchecked + { + this.X += dx; + this.Y += dy; + } + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(Point point) => this.Offset(point.X, point.Y); + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y); + + /// + public override string ToString() => $"Point [ X={this.X}, Y={this.Y} ]"; + + /// + public override bool Equals(object obj) => obj is Point other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); + + private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff)); + + private static short LowInt16(int n) => unchecked((short)(n & 0xffff)); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/PointF.cs b/src/SixLabors.Core/Primitives/PointF.cs new file mode 100644 index 0000000000..4a2da5cdcd --- /dev/null +++ b/src/SixLabors.Core/Primitives/PointF.cs @@ -0,0 +1,293 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Represents an ordered pair of single precision floating point x- and y-coordinates that defines a point in + /// a two-dimensional plane. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct PointF : IEquatable + { + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly PointF Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the point. + /// The vertical position of the point. + public PointF(float x, float y) + : this() + { + this.X = x; + this.Y = y; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The size. + public PointF(SizeF size) + { + this.X = size.Width; + this.Y = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public float X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public float Y { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The vector. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator PointF(Vector2 vector) => new PointF(vector.X, vector.Y); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(PointF point) => new Vector2(point.X, point.Y); + + /// + /// Creates a with the coordinates of the specified by truncating each of the coordinates. + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Point(PointF point) => Point.Truncate(point); + + /// + /// Negates the given point by multiplying all values by -1. + /// + /// The source point. + /// The negated point. + public static PointF operator -(PointF value) => new PointF(-value.X, -value.Y); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator +(PointF point, SizeF size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator -(PointF point, PointF size) => Subtract(point, size); + + /// + /// Translates a by a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator +(PointF point, PointF size) => Add(point, size); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF operator -(PointF point, SizeF size) => Subtract(point, size); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static PointF operator *(float left, PointF right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static PointF operator *(PointF left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static PointF operator /(PointF left, float right) + => new PointF(left.X / right, left.Y / right); + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(PointF left, PointF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(PointF left, PointF right) => !left.Equals(right); + + /// + /// Translates a by the given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Add(PointF point, SizeF size) => new PointF(point.X + size.Width, point.Y + size.Height); + + /// + /// Translates a by the given . + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Add(PointF point, PointF pointb) => new PointF(point.X + pointb.X, point.Y + pointb.Y); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Subtract(PointF point, SizeF size) => new PointF(point.X - size.Width, point.Y - size.Height); + + /// + /// Translates a by the negative of a given . + /// + /// The point on the left hand of the operand. + /// The point on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Subtract(PointF point, PointF pointb) => new PointF(point.X - pointb.X, point.Y - pointb.Y); + + /// + /// Translates a by the multiplying the X and Y by the given value. + /// + /// The point on the left hand of the operand. + /// The value on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Multiply(PointF point, float right) => new PointF(point.X * right, point.Y * right); + + /// + /// Transforms a point by a specified 3x2 matrix. + /// + /// The point to transform. + /// The transformation matrix used. + /// The transformed . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix); + + /// + /// Deconstructs this point into two floats. + /// + /// The out value for X. + /// The out value for Y. + public void Deconstruct(out float x, out float y) + { + x = this.X; + y = this.Y; + } + + /// + /// Translates this by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(float dx, float dy) + { + this.X += dx; + this.Y += dy; + } + + /// + /// Translates this by the specified amount. + /// + /// The used offset this . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(PointF point) => this.Offset(point.X, point.Y); + + /// + public override int GetHashCode() => HashCode.Combine(this.X, this.Y); + + /// + public override string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]"; + + /// + public override bool Equals(object obj) => obj is PointF && this.Equals((PointF)obj); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/Rectangle.cs b/src/SixLabors.Core/Primitives/Rectangle.cs new file mode 100644 index 0000000000..8600e2e4c8 --- /dev/null +++ b/src/SixLabors.Core/Primitives/Rectangle.cs @@ -0,0 +1,463 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Stores a set of four integers that represent the location and size of a rectangle. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Rectangle : IEquatable + { + /// + /// Represents a that has X, Y, Width, and Height values set to zero. + /// + public static readonly Rectangle Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public Rectangle(int x, int y, int width, int height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The which specifies the rectangles point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangles height and width. + /// + public Rectangle(Point point, Size size) + { + this.X = point.X; + this.Y = point.Y; + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public int Y { get; set; } + + /// + /// Gets or sets the width of this . + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public int Height { get; set; } + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Point Location + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new Point(this.X, this.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.X = value.X; + this.Y = value.Y; + } + } + + /// + /// Gets or sets the size of this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Size Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new Size(this.Width, this.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Width = value.Width; + this.Height = value.Height; + } + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Gets the y-coordinate of the top edge of this . + /// + public int Top => this.Y; + + /// + /// Gets the x-coordinate of the right edge of this . + /// + public int Right + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => unchecked(this.X + this.Width); + } + + /// + /// Gets the y-coordinate of the bottom edge of this . + /// + public int Bottom + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => unchecked(this.Y + this.Height); + } + + /// + /// Gets the x-coordinate of the left edge of this . + /// + public int Left => this.X; + + /// + /// Creates a with the coordinates of the specified . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator RectangleF(Rectangle rectangle) => new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector4(Rectangle rectangle) => new Vector4(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Rectangle left, Rectangle right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Rectangle left, Rectangle right) => !left.Equals(right); + + /// + /// Creates a new with the specified location and size. + /// The left coordinate of the rectangle. + /// The top coordinate of the rectangle. + /// The right coordinate of the rectangle. + /// The bottom coordinate of the rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + // ReSharper disable once InconsistentNaming + public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top)); + + /// + /// Returns the center point of the given . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point Center(Rectangle rectangle) => new Point(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2)); + + /// + /// Creates a rectangle that represents the intersection between and + /// . If there is no intersection, an empty rectangle is returned. + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Intersect(Rectangle a, Rectangle b) + { + int x1 = Math.Max(a.X, b.X); + int x2 = Math.Min(a.Right, b.Right); + int y1 = Math.Max(a.Y, b.Y); + int y2 = Math.Min(a.Bottom, b.Bottom); + + if (x2 >= x1 && y2 >= y1) + { + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + return Empty; + } + + /// + /// Creates a that is inflated by the specified amount. + /// + /// The rectangle. + /// The amount to inflate the width by. + /// The amount to inflate the height by. + /// A new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Inflate(Rectangle rectangle, int x, int y) + { + Rectangle r = rectangle; + r.Inflate(x, y); + return r; + } + + /// + /// Converts a to a by performing a ceiling operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Ceiling(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)MathF.Ceiling(rectangle.X), + (int)MathF.Ceiling(rectangle.Y), + (int)MathF.Ceiling(rectangle.Width), + (int)MathF.Ceiling(rectangle.Height)); + } + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed rectangle. + public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix) + { + PointF bottomRight = Point.Transform(new Point(rectangle.Right, rectangle.Bottom), matrix); + PointF topLeft = Point.Transform(rectangle.Location, matrix); + return new RectangleF(topLeft, new SizeF(bottomRight - topLeft)); + } + + /// + /// Converts a to a by performing a truncate operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Truncate(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)rectangle.X, + (int)rectangle.Y, + (int)rectangle.Width, + (int)rectangle.Height); + } + } + + /// + /// Converts a to a by performing a round operation on all the coordinates. + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Round(RectangleF rectangle) + { + unchecked + { + return new Rectangle( + (int)MathF.Round(rectangle.X), + (int)MathF.Round(rectangle.Y), + (int)MathF.Round(rectangle.Width), + (int)MathF.Round(rectangle.Height)); + } + } + + /// + /// Creates a rectangle that represents the union between and . + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rectangle Union(Rectangle a, Rectangle b) + { + int x1 = Math.Min(a.X, b.X); + int x2 = Math.Max(a.Right, b.Right); + int y1 = Math.Min(a.Y, b.Y); + int y2 = Math.Max(a.Bottom, b.Bottom); + + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Deconstructs this rectangle into four integers. + /// + /// The out value for X. + /// The out value for Y. + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out int x, out int y, out int width, out int height) + { + x = this.X; + y = this.Y; + width = this.Width; + height = this.Height; + } + + /// + /// Creates a Rectangle that represents the intersection between this Rectangle and the . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Intersect(Rectangle rectangle) + { + Rectangle result = Intersect(rectangle, this); + + this.X = result.X; + this.Y = result.Y; + this.Width = result.Width; + this.Height = result.Height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The width. + /// The height. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(int width, int height) + { + unchecked + { + this.X -= width; + this.Y -= height; + + this.Width += 2 * width; + this.Height += 2 * height; + } + } + + /// + /// Inflates this by the specified amount. + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(Size size) => this.Inflate(size.Width, size.Height); + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(int x, int y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this . + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Point point) => this.Contains(point.X, point.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained + /// within the rectangular region represented by this . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Rectangle rectangle) => + (this.X <= rectangle.X) && (rectangle.Right <= this.Right) && + (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); + + /// + /// Determines if the specfied intersects the rectangular region defined by + /// this . + /// + /// The other Rectange. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsWith(Rectangle rectangle) => + (rectangle.X < this.Right) && (this.X < rectangle.Right) && + (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(Point point) => this.Offset(point.X, point.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(int dx, int dy) + { + unchecked + { + this.X += dx; + this.Y += dy; + } + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.X, this.Y, this.Width, this.Height); + } + + /// + public override string ToString() + { + return $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]"; + } + + /// + public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Rectangle other) => + this.X.Equals(other.X) && + this.Y.Equals(other.Y) && + this.Width.Equals(other.Width) && + this.Height.Equals(other.Height); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/RectangleF.cs b/src/SixLabors.Core/Primitives/RectangleF.cs new file mode 100644 index 0000000000..c8b58f4ede --- /dev/null +++ b/src/SixLabors.Core/Primitives/RectangleF.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Stores a set of four single precision floating points that represent the location and size of a rectangle. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct RectangleF : IEquatable + { + /// + /// Represents a that has X, Y, Width, and Height values set to zero. + /// + public static readonly RectangleF Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal position of the rectangle. + /// The vertical position of the rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + public RectangleF(float x, float y, float width, float height) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The which specifies the rectangles point in a two-dimensional plane. + /// + /// + /// The which specifies the rectangles height and width. + /// + public RectangleF(PointF point, SizeF size) + { + this.X = point.X; + this.Y = point.Y; + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Gets or sets the x-coordinate of this . + /// + public float X { get; set; } + + /// + /// Gets or sets the y-coordinate of this . + /// + public float Y { get; set; } + + /// + /// Gets or sets the width of this . + /// + public float Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public float Height { get; set; } + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public PointF Location + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new PointF(this.X, this.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.X = value.X; + this.Y = value.Y; + } + } + + /// + /// Gets or sets the size of this . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public SizeF Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new SizeF(this.Width, this.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + this.Width = value.Width; + this.Height = value.Height; + } + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => (this.Width <= 0) || (this.Height <= 0); + + /// + /// Gets the y-coordinate of the top edge of this . + /// + public float Top => this.Y; + + /// + /// Gets the x-coordinate of the right edge of this . + /// + public float Right + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.X + this.Width; + } + + /// + /// Gets the y-coordinate of the bottom edge of this . + /// + public float Bottom + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Y + this.Height; + } + + /// + /// Gets the x-coordinate of the left edge of this . + /// + public float Left => this.X; + + /// + /// Creates a with the coordinates of the specified by truncating each coordinate. + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Rectangle(RectangleF rectangle) => Rectangle.Truncate(rectangle); + + /// + /// Compares two objects for equality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(RectangleF left, RectangleF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The on the left side of the operand. + /// The on the right side of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(RectangleF left, RectangleF right) => !left.Equals(right); + + /// + /// Creates a new with the specified location and size. + /// The left coordinate of the rectangle. + /// The top coordinate of the rectangle. + /// The right coordinate of the rectangle. + /// The bottom coordinate of the rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + // ReSharper disable once InconsistentNaming + public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new RectangleF(left, top, right - left, bottom - top); + + /// + /// Returns the center point of the given . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PointF Center(RectangleF rectangle) => new PointF(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2)); + + /// + /// Creates a rectangle that represents the intersection between and + /// . If there is no intersection, an empty rectangle is returned. + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Intersect(RectangleF a, RectangleF b) + { + float x1 = MathF.Max(a.X, b.X); + float x2 = MathF.Min(a.Right, b.Right); + float y1 = MathF.Max(a.Y, b.Y); + float y2 = MathF.Min(a.Bottom, b.Bottom); + + if (x2 >= x1 && y2 >= y1) + { + return new RectangleF(x1, y1, x2 - x1, y2 - y1); + } + + return Empty; + } + + /// + /// Creates a that is inflated by the specified amount. + /// + /// The rectangle. + /// The amount to inflate the width by. + /// The amount to inflate the height by. + /// A new . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Inflate(RectangleF rectangle, float x, float y) + { + RectangleF r = rectangle; + r.Inflate(x, y); + return r; + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle. + /// The transformation matrix. + /// A transformed . + public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix) + { + PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix); + PointF topLeft = PointF.Transform(rectangle.Location, matrix); + return new RectangleF(topLeft, new SizeF(bottomRight - topLeft)); + } + + /// + /// Creates a rectangle that represents the union between and . + /// + /// The first rectangle. + /// The second rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RectangleF Union(RectangleF a, RectangleF b) + { + float x1 = MathF.Min(a.X, b.X); + float x2 = MathF.Max(a.Right, b.Right); + float y1 = MathF.Min(a.Y, b.Y); + float y2 = MathF.Max(a.Bottom, b.Bottom); + + return new RectangleF(x1, y1, x2 - x1, y2 - y1); + } + + /// + /// Deconstructs this rectangle into four floats. + /// + /// The out value for X. + /// The out value for Y. + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out float x, out float y, out float width, out float height) + { + x = this.X; + y = this.Y; + width = this.Width; + height = this.Height; + } + + /// + /// Creates a RectangleF that represents the intersection between this RectangleF and the . + /// + /// The rectangle. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Intersect(RectangleF rectangle) + { + RectangleF result = Intersect(rectangle, this); + + this.X = result.X; + this.Y = result.Y; + this.Width = result.Width; + this.Height = result.Height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The width. + /// The height. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(float width, float height) + { + this.X -= width; + this.Y -= height; + + this.Width += 2 * width; + this.Height += 2 * height; + } + + /// + /// Inflates this by the specified amount. + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Inflate(SizeF size) => this.Inflate(size.Width, size.Height); + + /// + /// Determines if the specfied point is contained within the rectangular region defined by + /// this . + /// + /// The x-coordinate of the given point. + /// The y-coordinate of the given point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(float x, float y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom; + + /// + /// Determines if the specified point is contained within the rectangular region defined by this . + /// + /// The point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(PointF point) => this.Contains(point.X, point.Y); + + /// + /// Determines if the rectangular region represented by is entirely contained + /// within the rectangular region represented by this . + /// + /// The rectangle. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(RectangleF rectangle) => + (this.X <= rectangle.X) && (rectangle.Right <= this.Right) && + (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom); + + /// + /// Determines if the specfied intersects the rectangular region defined by + /// this . + /// + /// The other Rectange. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsWith(RectangleF rectangle) => + (rectangle.X < this.Right) && (this.X < rectangle.Right) && + (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(PointF point) => this.Offset(point.X, point.Y); + + /// + /// Adjusts the location of this rectangle by the specified amount. + /// + /// The amount to offset the x-coordinate. + /// The amount to offset the y-coordinate. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Offset(float dx, float dy) + { + this.X += dx; + this.Y += dy; + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.X, this.Y, this.Width, this.Height); + } + + /// + public override string ToString() + { + return $"RectangleF [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]"; + } + + /// + public override bool Equals(object obj) => obj is RectangleF other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(RectangleF other) => + this.X.Equals(other.X) && + this.Y.Equals(other.Y) && + this.Width.Equals(other.Width) && + this.Height.Equals(other.Height); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/Size.cs b/src/SixLabors.Core/Primitives/Size.cs new file mode 100644 index 0000000000..e0eb6484ce --- /dev/null +++ b/src/SixLabors.Core/Primitives/Size.cs @@ -0,0 +1,296 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Stores an ordered pair of integers, which specify a height and width. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct Size : IEquatable + { + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly Size Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The width and height of the size. + public Size(int value) + : this() + { + this.Width = value; + this.Height = value; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + public Size(int width, int height) + { + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size. + public Size(Size size) + : this() + { + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The point. + public Size(Point point) + { + this.Width = point.X; + this.Height = point.Y; + } + + /// + /// Gets or sets the width of this . + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public int Height { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the dimensions of the specified . + /// + /// The point. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator SizeF(Size size) => new SizeF(size.Width, size.Height); + + /// + /// Converts the given into a . + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Point(Size size) => new Point(size.Width, size.Height); + + /// + /// Computes the sum of adding two sizes. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator +(Size left, Size right) => Add(left, right); + + /// + /// Computes the difference left by subtracting one size from another. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size operator -(Size left, Size right) => Subtract(left, right); + + /// + /// Multiplies a by an producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static Size operator *(int left, Size right) => Multiply(right, left); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static Size operator *(Size left, int right) => Multiply(left, right); + + /// + /// Divides by an producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static Size operator /(Size left, int right) => new Size(unchecked(left.Width / right), unchecked(left.Height / right)); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static SizeF operator *(float left, Size right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF operator *(Size left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static SizeF operator /(Size left, float right) + => new SizeF(left.Width / right, left.Height / right); + + /// + /// Compares two objects for equality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Size left, Size right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Size left, Size right) => !left.Equals(right); + + /// + /// Performs vector addition of two objects. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Add(Size left, Size right) => new Size(unchecked(left.Width + right.Width), unchecked(left.Height + right.Height)); + + /// + /// Contracts a by another . + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Subtract(Size left, Size right) => new Size(unchecked(left.Width - right.Width), unchecked(left.Height - right.Height)); + + /// + /// Converts a to a by performing a ceiling operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Ceiling(SizeF size) => new Size(unchecked((int)MathF.Ceiling(size.Width)), unchecked((int)MathF.Ceiling(size.Height))); + + /// + /// Converts a to a by performing a round operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Round(SizeF size) => new Size(unchecked((int)MathF.Round(size.Width)), unchecked((int)MathF.Round(size.Height))); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size. + /// The transformation matrix. + /// A transformed size. + public static SizeF Transform(Size size, Matrix3x2 matrix) + { + var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } + + /// + /// Converts a to a by performing a round operation on all the dimensions. + /// + /// The size. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size Truncate(SizeF size) => new Size(unchecked((int)size.Width), unchecked((int)size.Height)); + + /// + /// Deconstructs this size into two integers. + /// + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out int width, out int height) + { + width = this.Width; + height = this.Height; + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Width, this.Height); + + /// + public override string ToString() => $"Size [ Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object obj) => obj is Size other && this.Equals(other); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Size other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + private static Size Multiply(Size size, int multiplier) => + new Size(unchecked(size.Width * multiplier), unchecked(size.Height * multiplier)); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type SizeF. + private static SizeF Multiply(Size size, float multiplier) => + new SizeF(size.Width * multiplier, size.Height * multiplier); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Primitives/SizeF.cs b/src/SixLabors.Core/Primitives/SizeF.cs new file mode 100644 index 0000000000..5d503a705d --- /dev/null +++ b/src/SixLabors.Core/Primitives/SizeF.cs @@ -0,0 +1,233 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.ComponentModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.Primitives +{ + /// + /// Stores an ordered pair of single precision floating points, which specify a height and width. + /// + /// + /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance, + /// as it avoids the need to create new values for modification operations. + /// + public struct SizeF : IEquatable + { + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly SizeF Empty = default; + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + public SizeF(float width, float height) + { + this.Width = width; + this.Height = height; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size. + public SizeF(SizeF size) + : this() + { + this.Width = size.Width; + this.Height = size.Height; + } + + /// + /// Initializes a new instance of the struct from the given . + /// + /// The point. + public SizeF(PointF point) + { + this.Width = point.X; + this.Height = point.Y; + } + + /// + /// Gets or sets the width of this . + /// + public float Width { get; set; } + + /// + /// Gets or sets the height of this . + /// + public float Height { get; set; } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => this.Equals(Empty); + + /// + /// Creates a with the coordinates of the specified . + /// + /// The point. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Vector2(SizeF point) => new Vector2(point.Width, point.Height); + + /// + /// Creates a with the dimensions of the specified by truncating each of the dimensions. + /// + /// The size. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Size(SizeF size) => new Size(unchecked((int)size.Width), unchecked((int)size.Height)); + + /// + /// Converts the given into a . + /// + /// The size. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator PointF(SizeF size) => new PointF(size.Width, size.Height); + + /// + /// Computes the sum of adding two sizes. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF operator +(SizeF left, SizeF right) => Add(left, right); + + /// + /// Computes the difference left by subtracting one size from another. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// The . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF operator -(SizeF left, SizeF right) => Subtract(left, right); + + /// + /// Multiplies by a producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static SizeF operator *(float left, SizeF right) => Multiply(right, left); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF operator *(SizeF left, float right) => Multiply(left, right); + + /// + /// Divides by a producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static SizeF operator /(SizeF left, float right) + => new SizeF(left.Width / right, left.Height / right); + + /// + /// Compares two objects for equality. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(SizeF left, SizeF right) => left.Equals(right); + + /// + /// Compares two objects for inequality. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(SizeF left, SizeF right) => !left.Equals(right); + + /// + /// Performs vector addition of two objects. + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF Add(SizeF left, SizeF right) => new SizeF(left.Width + right.Width, left.Height + right.Height); + + /// + /// Contracts a by another . + /// + /// The size on the left hand of the operand. + /// The size on the right hand of the operand. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SizeF Subtract(SizeF left, SizeF right) => new SizeF(left.Width - right.Width, left.Height - right.Height); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size. + /// The transformation matrix. + /// A transformed size. + public static SizeF Transform(SizeF size, Matrix3x2 matrix) + { + var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } + + /// + /// Deconstructs this size into two floats. + /// + /// The out value for the width. + /// The out value for the height. + public void Deconstruct(out float width, out float height) + { + width = this.Width; + height = this.Height; + } + + /// + public override int GetHashCode() => HashCode.Combine(this.Width, this.Height); + + /// + public override string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]"; + + /// + public override bool Equals(object obj) => obj is SizeF && this.Equals((SizeF)obj); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); + + /// + /// Multiplies by a producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type SizeF. + private static SizeF Multiply(SizeF size, float multiplier) => + new SizeF(size.Width * multiplier, size.Height * multiplier); + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs b/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs new file mode 100644 index 0000000000..0b0d33090f --- /dev/null +++ b/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs @@ -0,0 +1,234 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// tell this file to enable debug conditional method calls, i.e. all the debug guard calls +#define DEBUG + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace SixLabors.Helpers.Tests +{ + public class DebugGuardTests + { + private class Foo + { + } + + [Fact] + public void AllStaticMethodsOnOnDebugGuardHaveDEBUGConditional() + { + IEnumerable methods = typeof(DebugGuard).GetTypeInfo().GetMethods() + .Where(x => x.IsStatic); + + foreach (MethodInfo m in methods) + { + IEnumerable attribs = m.GetCustomAttributes(); + Assert.True(attribs.Select(x => x.ConditionString).Contains("DEBUG"), $"Method '{m.Name}' does not have [Conditional(\"DEBUG\")] set."); + } + } + + [Fact] + public void NotNull_WhenNull_Throws() + { + Foo foo = null; + Assert.Throws(() => Guard.NotNull(foo, nameof(foo))); + } + + [Fact] + public void NotNull_WhenNotNull() + { + var foo = new Foo(); + Guard.NotNull(foo, nameof(foo)); + } + + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData(" ", true)] + [InlineData("$", false)] + [InlineData("lol", false)] + public void NotNullOrWhiteSpace(string str, bool shouldThrow) + { + if (shouldThrow) + { + Assert.ThrowsAny(() => Guard.NotNullOrWhiteSpace(str, nameof(str))); + } + else + { + Guard.NotNullOrWhiteSpace(str, nameof(str)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsTrue(bool value) + { + if (!value) + { + Assert.Throws(() => Guard.IsTrue(value, nameof(value), "Boo!")); + } + else + { + Guard.IsTrue(value, nameof(value), "Boo."); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsFalse(bool value) + { + if (value) + { + Assert.Throws(() => Guard.IsFalse(value, nameof(value), "Boo!")); + } + else + { + Guard.IsFalse(value, nameof(value), "Boo."); + } + } + + public static readonly TheoryData SizeCheckData = new TheoryData + { + { 0, 0, false }, + { 1, 1, false }, + { 1, 0, false }, + { 13, 13, false }, + { 20, 13, false }, + { 12, 13, true }, + { 0, 1, true }, + }; + + [Theory] + [MemberData(nameof(SizeCheckData))] + public void MustBeSizedAtLeast(int length, int minLength, bool shouldThrow) + { + int[] data = new int[length]; + + if (shouldThrow) + { + Assert.Throws(() => Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data))); + Assert.Throws(() => Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data))); + } + else + { + Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data)); + Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data)); + } + } + + [Theory] + [MemberData(nameof(SizeCheckData))] + public void DestinationShouldNotBeTooShort(int destLength, int sourceLength, bool shouldThrow) + { + int[] dest = new int[destLength]; + int[] source = new int[sourceLength]; + + if (shouldThrow) + { + Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest))); + Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest))); + } + else + { + Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest)); + Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest)); + } + } + + [Fact] + public void MustBeLessThan_IsLess_ThrowsNoException() + { + DebugGuard.MustBeLessThan(0, 1, "myParamName"); + } + + [Theory] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void MustBeLessThan_IsGreaterOrEqual_ThrowsNoException(int value, int max) + { + ArgumentOutOfRangeException exception = Assert.Throws( + () => DebugGuard.MustBeLessThan(value, max, "myParamName")); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value {value} must be less than {max}.", exception.Message); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 1)] + public void MustBeLessThanOrEqualTo_IsLessOrEqual_ThrowsNoException(int value, int max) + { + DebugGuard.MustBeLessThanOrEqualTo(value, max, "myParamName"); + } + + [Fact] + public void MustBeLessThanOrEqualTo_IsGreater_ThrowsNoException() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => DebugGuard.MustBeLessThanOrEqualTo(2, 1, "myParamName")); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value 2 must be less than or equal to 1.", exception.Message); + } + + [Fact] + public void MustBeGreaterThan_IsGreater_ThrowsNoException() + { + DebugGuard.MustBeGreaterThan(2, 1, "myParamName"); + } + + [Theory] + [InlineData(1, 2)] + [InlineData(1, 1)] + public void MustBeGreaterThan_IsLessOrEqual_ThrowsNoException(int value, int min) + { + ArgumentOutOfRangeException exception = Assert.Throws( + () => DebugGuard.MustBeGreaterThan(value, min, "myParamName")); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value {value} must be greater than {min}.", exception.Message); + } + + [Theory] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void MustBeGreaterThanOrEqualTo_IsGreaterOrEqual_ThrowsNoException(int value, int min) + { + DebugGuard.MustBeGreaterThanOrEqualTo(value, min, "myParamName"); + } + + [Fact] + public void MustBeGreaterThanOrEqualTo_IsLess_ThrowsNoException() + { + ArgumentOutOfRangeException exception = Assert.Throws( + () => DebugGuard.MustBeGreaterThanOrEqualTo(1, 2, "myParamName")); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value 1 must be greater than or equal to 2.", exception.Message); + } + + [Theory] + [InlineData(new int[] { 1, 2 }, 1)] + [InlineData(new int[] { 1, 2 }, 2)] + public void MustBeSizedAtLeast_Array_LengthIsGreaterOrEqual_ThrowsNoException(int[] value, int minLength) + { + DebugGuard.MustBeSizedAtLeast(value, minLength, "myParamName"); + } + + [Fact] + public void MustBeSizedAtLeast_Array_LengthIsLess_ThrowsException() + { + ArgumentException exception = Assert.Throws( + () => DebugGuard.MustBeSizedAtLeast(new int[] { 1, 2 }, 3, "myParamName")); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"The size must be at least 3.", exception.Message); + } + } +} diff --git a/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs b/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs new file mode 100644 index 0000000000..15220d4b14 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SixLabors.Tests.Helpers +{ + /// + /// Allows the comparison of single-precision floating point values by precision. + /// + public struct FloatRoundingComparer : IEqualityComparer, IEqualityComparer + { + /// + /// Initializes a new instance of the struct. + /// + /// The number of decimal places (valid values: 0-7). + public FloatRoundingComparer(int precision) + { + Guard.MustBeBetweenOrEqualTo(precision, 0, 7, nameof(precision)); + this.Precision = precision; + } + + /// + /// Gets the number of decimal places (valid values: 0-7). + /// + public int Precision { get; } + + /// + public bool Equals(float x, float y) + { + float xp = (float)Math.Round(x, this.Precision, MidpointRounding.AwayFromZero); + float yp = (float)Math.Round(y, this.Precision, MidpointRounding.AwayFromZero); + + return Comparer.Default.Compare(xp, yp) == 0; + } + + /// + public bool Equals(Vector4 x, Vector4 y) + { + return this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W); + } + + /// + public int GetHashCode(float obj) + { + unchecked + { + int hashCode = obj.GetHashCode(); + hashCode = (hashCode * 397) ^ this.Precision.GetHashCode(); + return hashCode; + } + } + + /// + public int GetHashCode(Vector4 obj) => HashCode.Combine(obj, this.Precision); + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs b/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs new file mode 100644 index 0000000000..93e4cec60a --- /dev/null +++ b/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using Xunit; + +namespace SixLabors.Tests.Helpers +{ + public class GeometryUtilitiesTests + { + [Fact] + public void Convert_Degree_To_Radian() + => Assert.Equal((float)(Math.PI / 2D), GeometryUtilities.DegreeToRadian(90F), new FloatRoundingComparer(6)); + + [Fact] + public void Convert_Radian_To_Degree() + => Assert.Equal(60F, GeometryUtilities.RadianToDegree((float)(Math.PI / 3D)), new FloatRoundingComparer(5)); + } +} diff --git a/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs b/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs new file mode 100644 index 0000000000..4b5ebbdfde --- /dev/null +++ b/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs @@ -0,0 +1,248 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using Xunit; + +namespace SixLabors.Helpers.Tests +{ + public class GuardTests + { + private class Foo + { + } + + [Fact] + public void NotNull_WhenNull_Throws() + { + Foo foo = null; + Assert.Throws(() => Guard.NotNull(foo, nameof(foo))); + } + + [Fact] + public void NotNull_WhenNotNull() + { + Foo foo = new Foo(); + Guard.NotNull(foo, nameof(foo)); + } + + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData(" ", true)] + [InlineData("$", false)] + [InlineData("lol", false)] + public void NotNullOrWhiteSpace(string str, bool shouldThrow) + { + if (shouldThrow) + { + Assert.ThrowsAny(() => Guard.NotNullOrWhiteSpace(str, nameof(str))); + } + else + { + Guard.NotNullOrWhiteSpace(str, nameof(str)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsTrue(bool value) + { + if (!value) + { + Assert.Throws(() => Guard.IsTrue(value, nameof(value), "Boo!")); + } + else + { + Guard.IsTrue(value, nameof(value), "Boo."); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsFalse(bool value) + { + if (value) + { + Assert.Throws(() => Guard.IsFalse(value, nameof(value), "Boo!")); + } + else + { + Guard.IsFalse(value, nameof(value), "Boo."); + } + } + + public static readonly TheoryData SizeCheckData = new TheoryData + { + { 0, 0, false }, + { 1, 1, false }, + { 1, 0, false }, + { 13, 13, false }, + { 20, 13, false }, + { 12, 13, true }, + { 0, 1, true }, + }; + + [Theory] + [MemberData(nameof(SizeCheckData))] + public void MustBeSizedAtLeast(int length, int minLength, bool shouldThrow) + { + int[] data = new int[length]; + + if (shouldThrow) + { + Assert.Throws(() => Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data))); + Assert.Throws(() => Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data))); + } + else + { + Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data)); + Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data)); + } + } + + [Theory] + [MemberData(nameof(SizeCheckData))] + public void DestinationShouldNotBeTooShort(int destLength, int sourceLength, bool shouldThrow) + { + int[] dest = new int[destLength]; + int[] source = new int[sourceLength]; + + if (shouldThrow) + { + Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest))); + Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest))); + } + else + { + Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest)); + Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest)); + } + } + + [Fact] + public void MustBeLessThan_IsLess_ThrowsNoException() + { + Guard.MustBeLessThan(0, 1, "myParamName"); + } + + [Theory] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void MustBeLessThan_IsGreaterOrEqual_ThrowsNoException(int value, int max) + { + ArgumentOutOfRangeException exception = Assert.Throws(() => + { + Guard.MustBeLessThan(value, max, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value {value} must be less than {max}.", exception.Message); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 1)] + public void MustBeLessThanOrEqualTo_IsLessOrEqual_ThrowsNoException(int value, int max) + { + Guard.MustBeLessThanOrEqualTo(value, max, "myParamName"); + } + + [Fact] + public void MustBeLessThanOrEqualTo_IsGreater_ThrowsNoException() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => + { + Guard.MustBeLessThanOrEqualTo(2, 1, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value 2 must be less than or equal to 1.", exception.Message); + } + + [Fact] + public void MustBeGreaterThan_IsGreater_ThrowsNoException() + { + Guard.MustBeGreaterThan(2, 1, "myParamName"); + } + + [Theory] + [InlineData(1, 2)] + [InlineData(1, 1)] + public void MustBeGreaterThan_IsLessOrEqual_ThrowsNoException(int value, int min) + { + ArgumentOutOfRangeException exception = Assert.Throws(() => + { + Guard.MustBeGreaterThan(value, min, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value {value} must be greater than {min}.", exception.Message); + } + + [Theory] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void MustBeGreaterThanOrEqualTo_IsGreaterOrEqual_ThrowsNoException(int value, int min) + { + Guard.MustBeGreaterThanOrEqualTo(value, min, "myParamName"); + } + + [Fact] + public void MustBeGreaterThanOrEqualTo_IsLess_ThrowsNoException() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => + { + Guard.MustBeGreaterThanOrEqualTo(1, 2, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value 1 must be greater than or equal to 2.", exception.Message); + } + + [Theory] + [InlineData(1, 1, 3)] + [InlineData(2, 1, 3)] + [InlineData(3, 1, 3)] + public void MustBeBetweenOrEqualTo_IsBetweenOrEqual_ThrowsNoException(int value, int min, int max) + { + Guard.MustBeBetweenOrEqualTo(value, min, max, "myParamName"); + } + + [Theory] + [InlineData(0, 1, 3)] + [InlineData(4, 1, 3)] + public void MustBeBetweenOrEqualTo_IsLessOrGreater_ThrowsNoException(int value, int min, int max) + { + ArgumentOutOfRangeException exception = Assert.Throws(() => + { + Guard.MustBeBetweenOrEqualTo(value, min, max, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains($"Value {value} must be greater than or equal to {min} and less than or equal to {max}.", exception.Message); + } + + [Theory] + [InlineData(2, 1)] + [InlineData(2, 2)] + public void MustBeSizedAtLeast_Array_LengthIsGreaterOrEqual_ThrowsNoException(int valueLength, int minLength) + { + Guard.MustBeSizedAtLeast(new int[valueLength], minLength, "myParamName"); + } + + [Fact] + public void MustBeSizedAtLeast_Array_LengthIsLess_ThrowsException() + { + ArgumentException exception = Assert.Throws(() => + { + Guard.MustBeSizedAtLeast(new int[] { 1, 2 }, 3, "myParamName"); + }); + + Assert.Equal("myParamName", exception.ParamName); + Assert.Contains("The size must be at least 3", exception.Message); + } + } +} diff --git a/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs b/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs new file mode 100644 index 0000000000..9ae95f0140 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using Xunit; + +namespace SixLabors.Tests.Helpers +{ + public class MathFTests + { + [Fact] + public void MathF_PI_Is_Equal() + { + Assert.Equal(MathF.PI, (float)Math.PI); + } + + [Fact] + public void MathF_Ceililng_Is_Equal() + { + Assert.Equal(MathF.Ceiling(0.3333F), (float)Math.Ceiling(0.3333F)); + } + + [Fact] + public void MathF_Cos_Is_Equal() + { + Assert.Equal(MathF.Cos(0.3333F), (float)Math.Cos(0.3333F)); + } + + [Fact] + public void MathF_Abs_Is_Equal() + { + Assert.Equal(MathF.Abs(-0.3333F), (float)Math.Abs(-0.3333F)); + } + + [Fact] + public void MathF_Atan2_Is_Equal() + { + Assert.Equal(MathF.Atan2(1.2345F, 1.2345F), (float)Math.Atan2(1.2345F, 1.2345F)); + } + + [Fact] + public void MathF_Exp_Is_Equal() + { + Assert.Equal(MathF.Exp(1.2345F), (float)Math.Exp(1.2345F)); + } + + [Fact] + public void MathF_Floor_Is_Equal() + { + Assert.Equal(MathF.Floor(1.2345F), (float)Math.Floor(1.2345F)); + } + + [Fact] + public void MathF_Min_Is_Equal() + { + Assert.Equal(MathF.Min(1.2345F, 5.4321F), (float)Math.Min(1.2345F, 5.4321F)); + } + + [Fact] + public void MathF_Max_Is_Equal() + { + Assert.Equal(MathF.Max(1.2345F, 5.4321F), (float)Math.Max(1.2345F, 5.4321F)); + } + + [Fact] + public void MathF_Pow_Is_Equal() + { + Assert.Equal(MathF.Pow(1.2345F, 5.4321F), (float)Math.Pow(1.2345F, 5.4321F)); + } + + [Fact] + public void MathF_Round_Is_Equal() + { + Assert.Equal(MathF.Round(1.2345F), (float)Math.Round(1.2345F)); + } + + [Fact] + public void MathF_Round_With_Midpoint_Is_Equal() + { + Assert.Equal(MathF.Round(1.2345F, MidpointRounding.AwayFromZero), (float)Math.Round(1.2345F, MidpointRounding.AwayFromZero)); + } + + [Fact] + public void MathF_Sin_Is_Equal() + { + Assert.Equal(MathF.Sin(1.2345F), (float)Math.Sin(1.2345F)); + } + + [Fact] + public void MathF_Sqrt_Is_Equal() + { + Assert.Equal(MathF.Sqrt(2F), (float)Math.Sqrt(2F)); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs new file mode 100644 index 0000000000..1c1d721674 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.Tests; +using Xunit; + +namespace SixLabors.Memory.Tests +{ + public class ArrayPoolMemoryAllocatorTests + { + private const int MaxPooledBufferSizeInBytes = 2048; + + private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2; + + private MemoryAllocator MemoryAllocator { get; set; } = + new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes); + + /// + /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location. + /// + private bool CheckIsRentingPooledBuffer(int length) + where T : struct + { + IMemoryOwner buffer = this.MemoryAllocator.Allocate(length); + ref T ptrToPrevPosition0 = ref buffer.GetReference(); + buffer.Dispose(); + + buffer = this.MemoryAllocator.Allocate(length); + bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref buffer.GetReference()); + buffer.Dispose(); + + return sameBuffers; + } + + public class BufferTests : BufferTestSuite + { + public BufferTests() + : base(new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes)) + { + } + } + + public class Constructor + { + [Fact] + public void WhenBothParametersPassedByUser() + { + var mgr = new ArrayPoolMemoryAllocator(1111, 666); + Assert.Equal(1111, mgr.MaxPoolSizeInBytes); + Assert.Equal(666, mgr.PoolSelectorThresholdInBytes); + } + + [Fact] + public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAutoCalculated() + { + var mgr = new ArrayPoolMemoryAllocator(5000); + Assert.Equal(5000, mgr.MaxPoolSizeInBytes); + Assert.True(mgr.PoolSelectorThresholdInBytes < mgr.MaxPoolSizeInBytes); + } + + [Fact] + public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown() + { + Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200)); + } + } + + [Theory] + [InlineData(32)] + [InlineData(512)] + [InlineData(MaxPooledBufferSizeInBytes - 1)] + public void SmallBuffersArePooled_OfByte(int size) + { + Assert.True(this.CheckIsRentingPooledBuffer(size)); + } + + [Theory] + [InlineData(128 * 1024 * 1024)] + [InlineData(MaxPooledBufferSizeInBytes + 1)] + public void LargeBuffersAreNotPooled_OfByte(int size) + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + Assert.False(this.CheckIsRentingPooledBuffer(size)); + } + + [Fact] + public unsafe void SmallBuffersArePooled_OfBigValueType() + { + int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1; + + Assert.True(this.CheckIsRentingPooledBuffer(count)); + } + + [Fact] + public unsafe void LaregeBuffersAreNotPooled_OfBigValueType() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) + 1; + + Assert.False(this.CheckIsRentingPooledBuffer(count)); + } + + [Theory] + [InlineData(AllocationOptions.None)] + [InlineData(AllocationOptions.Clean)] + public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options) + { + using (IMemoryOwner firstAlloc = this.MemoryAllocator.Allocate(42)) + { + firstAlloc.GetSpan().Fill(666); + } + + using (IMemoryOwner secondAlloc = this.MemoryAllocator.Allocate(42, options)) + { + int expected = options == AllocationOptions.Clean ? 0 : 666; + Assert.Equal(expected, secondAlloc.GetSpan()[0]); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReleaseRetainedResources_ReplacesInnerArrayPool(bool keepBufferAlive) + { + IMemoryOwner buffer = this.MemoryAllocator.Allocate(32); + ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan()); + + if (!keepBufferAlive) + { + buffer.Dispose(); + } + + this.MemoryAllocator.ReleaseRetainedResources(); + + buffer = this.MemoryAllocator.Allocate(32); + + Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference())); + } + + [Fact] + public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() + { + IMemoryOwner buffer = this.MemoryAllocator.Allocate(32); + this.MemoryAllocator.ReleaseRetainedResources(); + buffer.Dispose(); + } + + [Fact] + public void AllocationOverLargeArrayThreshold_UsesDifferentPool() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int); + + IMemoryOwner small = this.MemoryAllocator.Allocate(ArrayLengthThreshold - 1); + ref int ptr2Small = ref small.GetReference(); + small.Dispose(); + + IMemoryOwner large = this.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); + + Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.GetReference())); + } + + [Fact] + public void CreateWithAggressivePooling() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling(); + + Assert.True(this.CheckIsRentingPooledBuffer(4096 * 4096)); + } + + [Fact] + public void CreateDefault() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault(); + + Assert.False(this.CheckIsRentingPooledBuffer(2 * 4096 * 4096)); + Assert.True(this.CheckIsRentingPooledBuffer(2048 * 2048)); + } + + [Fact] + public void CreateWithModeratePooling() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); + + Assert.False(this.CheckIsRentingPooledBuffer(2048 * 2048)); + Assert.True(this.CheckIsRentingPooledBuffer(1024 * 16)); + } + + [StructLayout(LayoutKind.Sequential)] + private struct Rgba32 + { + private readonly uint dummy; + } + + private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5; + + [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)] + private struct LargeStruct + { + } + + [Theory] + [InlineData(-1)] + [InlineData((int.MaxValue / SizeOfLargeStruct) + 1)] + public void AllocateIncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + { + ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length)); + Assert.Equal("length", ex.ParamName); + } + + [Theory] + [InlineData(-1)] + public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + { + ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length)); + Assert.Equal("length", ex.ParamName); + } + } +} diff --git a/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs new file mode 100644 index 0000000000..fd53db1d8d --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.Memory.Tests +{ + internal static class BufferExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetSpan(this IMemoryOwner buffer) + => buffer.Memory.Span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Length(this IMemoryOwner buffer) + => buffer.GetSpan().Length; + + public static ref T GetReference(this IMemoryOwner buffer) + where T : struct => + ref MemoryMarshal.GetReference(buffer.GetSpan()); + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs new file mode 100644 index 0000000000..eca1842646 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.Memory.Tests +{ + /// + /// Inherit this class to test an implementation (provided by ). + /// + public abstract class BufferTestSuite + { + protected BufferTestSuite(MemoryAllocator memoryAllocator) + { + this.MemoryAllocator = memoryAllocator; + } + + protected MemoryAllocator MemoryAllocator { get; } + + public struct CustomStruct : IEquatable + { + public long A; + + public byte B; + + public float C; + + public CustomStruct(long a, byte b, float c) + { + this.A = a; + this.B = b; + this.C = c; + } + + public bool Equals(CustomStruct other) + { + return this.A == other.A && this.B == other.B && this.C.Equals(other.C); + } + + public override bool Equals(object obj) + { + return obj is CustomStruct other && this.Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = this.A.GetHashCode(); + hashCode = (hashCode * 397) ^ this.B.GetHashCode(); + hashCode = (hashCode * 397) ^ this.C.GetHashCode(); + return hashCode; + } + } + } + + public static readonly TheoryData LenthValues = new TheoryData { 0, 1, 7, 1023, 1024 }; + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_byte(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_float(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_CustomStruct(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + private void TestHasCorrectLength(int desiredLength) + where T : struct + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength)) + { + Assert.Equal(desiredLength, buffer.GetSpan().Length); + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_byte(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength, false); + this.TestCanAllocateCleanBuffer(desiredLength, true); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_double(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_CustomStruct(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength); + } + + private IMemoryOwner Allocate(int desiredLength, AllocationOptions options, bool managedByteBuffer) + where T : struct + { + if (managedByteBuffer) + { + if (!(this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength, options) is IMemoryOwner buffer)) + { + throw new InvalidOperationException("typeof(T) != typeof(byte)"); + } + + return buffer; + } + + return this.MemoryAllocator.Allocate(desiredLength, options); + } + + private void TestCanAllocateCleanBuffer(int desiredLength, bool testManagedByteBuffer = false) + where T : struct, IEquatable + { + ReadOnlySpan expected = new T[desiredLength]; + + for (int i = 0; i < 10; i++) + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.Clean, testManagedByteBuffer)) + { + Assert.True(buffer.GetSpan().SequenceEqual(expected)); + } + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void SpanPropertyIsAlwaysTheSame_int(int desiredLength) + { + this.TestSpanPropertyIsAlwaysTheSame(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength) + { + this.TestSpanPropertyIsAlwaysTheSame(desiredLength, false); + this.TestSpanPropertyIsAlwaysTheSame(desiredLength, true); + } + + private void TestSpanPropertyIsAlwaysTheSame(int desiredLength, bool testManagedByteBuffer = false) + where T : struct + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + ref T a = ref MemoryMarshal.GetReference(buffer.GetSpan()); + ref T b = ref MemoryMarshal.GetReference(buffer.GetSpan()); + ref T c = ref MemoryMarshal.GetReference(buffer.GetSpan()); + + Assert.True(Unsafe.AreSame(ref a, ref b)); + Assert.True(Unsafe.AreSame(ref b, ref c)); + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void WriteAndReadElements_float(int desiredLength) + { + this.TestWriteAndReadElements(desiredLength, x => x * 1.2f); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void WriteAndReadElements_byte(int desiredLength) + { + this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), false); + this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), true); + } + + private void TestWriteAndReadElements(int desiredLength, Func getExpectedValue, bool testManagedByteBuffer = false) + where T : struct + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + T[] expectedVals = new T[buffer.Length()]; + + for (int i = 0; i < buffer.Length(); i++) + { + Span span = buffer.GetSpan(); + expectedVals[i] = getExpectedValue(i); + span[i] = expectedVals[i]; + } + + for (int i = 0; i < buffer.Length(); i++) + { + Span span = buffer.GetSpan(); + Assert.Equal(expectedVals[i], span[i]); + } + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength, false); + this.TestIndexOutOfRangeShouldThrow(desiredLength, true); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_long(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_CustomStruct(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength); + } + + private T TestIndexOutOfRangeShouldThrow(int desiredLength, bool testManagedByteBuffer = false) + where T : struct, IEquatable + { + var dummy = default(T); + + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength]; + }); + + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength + 1]; + }); + + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength + 42]; + }); + } + + return dummy; + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + [InlineData(1024)] + [InlineData(6666)] + public void ManagedByteBuffer_ArrayIsCorrect(int desiredLength) + { + using (IManagedByteBuffer buffer = this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength)) + { + ref byte array0 = ref buffer.Array[0]; + ref byte span0 = ref buffer.GetReference(); + + Assert.True(Unsafe.AreSame(ref span0, ref array0)); + Assert.True(buffer.Array.Length >= buffer.GetSpan().Length); + } + } + + [Fact] + public void GetMemory_ReturnsValidMemory() + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42)) + { + Span span0 = buffer.GetSpan(); + span0[10].A = 30; + Memory memory = buffer.Memory; + + Assert.Equal(42, memory.Length); + Span span1 = memory.Span; + + Assert.Equal(42, span1.Length); + Assert.Equal(30, span1[10].A); + } + } + + [Fact] + public unsafe void GetMemory_ResultIsPinnable() + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42)) + { + Span span0 = buffer.GetSpan(); + span0[10] = 30; + + Memory memory = buffer.Memory; + + using (MemoryHandle h = memory.Pin()) + { + int* ptr = (int*)h.Pointer; + Assert.Equal(30, ptr[10]); + } + } + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs new file mode 100644 index 0000000000..a22e9e375d --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using Xunit; + +namespace SixLabors.Memory.Tests +{ + public class SimpleGcMemoryAllocatorTests + { + public class BufferTests : BufferTestSuite + { + public BufferTests() + : base(new SimpleGcMemoryAllocator()) + { + } + } + + protected SimpleGcMemoryAllocator MemoryAllocator { get; } = new SimpleGcMemoryAllocator(); + + [Theory] + [InlineData(-1)] + public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + { + ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length)); + Assert.Equal("length", ex.ParamName); + } + + [Theory] + [InlineData(-1)] + public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + { + ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length)); + Assert.Equal("length", ex.ParamName); + } + + [StructLayout(LayoutKind.Explicit, Size = 512)] + private struct BigStruct + { + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs b/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs new file mode 100644 index 0000000000..f78a18fc13 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + public class PointFTests + { + [Fact] + public void CanReinterpretCastFromVector2() + { + var vector = new Vector2(1, 2); + + PointF point = Unsafe.As(ref vector); + + Assert.Equal(vector.X, point.X); + Assert.Equal(vector.Y, point.Y); + } + + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, PointF.Empty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(float.MinValue, float.MaxValue)] + [InlineData(0.0, 0.0)] + public void NonDefaultConstructorTest(float x, float y) + { + var p1 = new PointF(x, y); + + Assert.Equal(x, p1.X); + Assert.Equal(y, p1.Y); + } + + [Fact] + public void IsEmptyDefaultsTest() + { + Assert.True(PointF.Empty.IsEmpty); + Assert.True(default(PointF).IsEmpty); + Assert.True(new PointF(0, 0).IsEmpty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + public void IsEmptyRandomTest(float x, float y) + { + Assert.False(new PointF(x, y).IsEmpty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void CoordinatesTest(float x, float y) + { + var p = new PointF(x, y); + Assert.Equal(x, p.X); + Assert.Equal(y, p.Y); + + p.X = 10; + Assert.Equal(10, p.X); + + p.Y = -10.123f; + Assert.Equal(-10.123, p.Y, 3); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue, int.MaxValue, int.MinValue)] + [InlineData(float.MinValue, float.MaxValue, int.MinValue, int.MaxValue)] + [InlineData(0, 0, 0, 0)] + public void ArithmeticTestWithSize(float x, float y, int x1, int y1) + { + var p = new PointF(x, y); + var s = new Size(x1, y1); + + var addExpected = new PointF(x + x1, y + y1); + var subExpected = new PointF(x - x1, y - y1); + Assert.Equal(addExpected, p + s); + Assert.Equal(subExpected, p - s); + Assert.Equal(addExpected, PointF.Add(p, s)); + Assert.Equal(subExpected, PointF.Subtract(p, s)); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MaxValue)] + [InlineData(0, 0)] + public void ArithmeticTestWithSizeF(float x, float y) + { + var p = new PointF(x, y); + var s = new SizeF(y, x); + + var addExpected = new PointF(x + y, y + x); + var subExpected = new PointF(x - y, y - x); + Assert.Equal(addExpected, p + s); + Assert.Equal(subExpected, p - s); + Assert.Equal(addExpected, PointF.Add(p, s)); + Assert.Equal(subExpected, PointF.Subtract(p, s)); + } + + [Fact] + public void RotateTest() + { + var p = new PointF(13, 17); + Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty); + + var pout = PointF.Transform(p, matrix); + + Assert.Equal(new PointF(-2.82842732F, 21.2132034F), pout); + } + + [Fact] + public void SkewTest() + { + var p = new PointF(13, 17); + Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, PointF.Empty); + + var pout = PointF.Transform(p, matrix); + Assert.Equal(new PointF(30, 30), pout); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MaxValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void EqualityTest(float x, float y) + { + var pLeft = new PointF(x, y); + var pRight = new PointF(y, x); + + if (x == y) + { + Assert.True(pLeft == pRight); + Assert.False(pLeft != pRight); + Assert.True(pLeft.Equals(pRight)); + Assert.True(pLeft.Equals((object)pRight)); + Assert.Equal(pLeft.GetHashCode(), pRight.GetHashCode()); + return; + } + + Assert.True(pLeft != pRight); + Assert.False(pLeft == pRight); + Assert.False(pLeft.Equals(pRight)); + Assert.False(pLeft.Equals((object)pRight)); + } + + [Fact] + public void EqualityTest_NotPointF() + { + var point = new PointF(0, 0); + Assert.False(point.Equals(null)); + Assert.False(point.Equals(0)); + + // If PointF implements IEquatable (e.g. in .NET Core), then structs that are implicitly + // convertible to var can potentially be equal. + // See https://github.com/dotnet/corefx/issues/5255. + bool expectsImplicitCastToPointF = typeof(IEquatable).IsAssignableFrom(point.GetType()); + Assert.Equal(expectsImplicitCastToPointF, point.Equals(new Point(0, 0))); + + Assert.False(point.Equals((object)new Point(0, 0))); // No implicit cast + } + + [Fact] + public void GetHashCodeTest() + { + var point = new PointF(10, 10); + Assert.Equal(point.GetHashCode(), new PointF(10, 10).GetHashCode()); + Assert.NotEqual(point.GetHashCode(), new PointF(20, 10).GetHashCode()); + Assert.NotEqual(point.GetHashCode(), new PointF(10, 20).GetHashCode()); + } + + [Fact] + public void ToStringTest() + { + var p = new PointF(5.1F, -5.123F); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "PointF [ X={0}, Y={1} ]", p.X, p.Y), p.ToString()); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void DeconstructTest(float x, float y) + { + PointF p = new PointF(x, y); + + (float deconstructedX, float deconstructedY) = p; + + Assert.Equal(x, deconstructedX); + Assert.Equal(y, deconstructedY); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Primitives/PointTests.cs b/tests/SixLabors.Core.Tests/Primitives/PointTests.cs new file mode 100644 index 0000000000..7ad7f0b62c --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/PointTests.cs @@ -0,0 +1,258 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using System.Numerics; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + public class PointTests + { + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, Point.Empty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void NonDefaultConstructorTest(int x, int y) + { + var p1 = new Point(x, y); + var p2 = new Point(new Size(x, y)); + + Assert.Equal(p1, p2); + } + + [Theory] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + [InlineData(0)] + public void SingleIntConstructorTest(int x) + { + var p1 = new Point(x); + var p2 = new Point(unchecked((short)(x & 0xFFFF)), unchecked((short)((x >> 16) & 0xFFFF))); + + Assert.Equal(p1, p2); + } + + [Fact] + public void IsEmptyDefaultsTest() + { + Assert.True(Point.Empty.IsEmpty); + Assert.True(default(Point).IsEmpty); + Assert.True(new Point(0, 0).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + public void IsEmptyRandomTest(int x, int y) + { + Assert.False(new Point(x, y).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void CoordinatesTest(int x, int y) + { + var p = new Point(x, y); + Assert.Equal(x, p.X); + Assert.Equal(y, p.Y); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void PointFConversionTest(int x, int y) + { + PointF p = new Point(x, y); + Assert.Equal(new PointF(x, y), p); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void SizeConversionTest(int x, int y) + { + var sz = (Size)new Point(x, y); + Assert.Equal(new Size(x, y), sz); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void ArithmeticTest(int x, int y) + { + Point addExpected, subExpected, p = new Point(x, y); + var s = new Size(y, x); + + unchecked + { + addExpected = new Point(x + y, y + x); + subExpected = new Point(x - y, y - x); + } + + Assert.Equal(addExpected, p + s); + Assert.Equal(subExpected, p - s); + Assert.Equal(addExpected, Point.Add(p, s)); + Assert.Equal(subExpected, Point.Subtract(p, s)); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void PointFMathematicalTest(float x, float y) + { + var pf = new PointF(x, y); + Point pCeiling, pTruncate, pRound; + + unchecked + { + pCeiling = new Point((int)MathF.Ceiling(x), (int)MathF.Ceiling(y)); + pTruncate = new Point((int)x, (int)y); + pRound = new Point((int)MathF.Round(x), (int)MathF.Round(y)); + } + + Assert.Equal(pCeiling, Point.Ceiling(pf)); + Assert.Equal(pRound, Point.Round(pf)); + Assert.Equal(pTruncate, (Point)pf); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void OffsetTest(int x, int y) + { + var p1 = new Point(x, y); + var p2 = new Point(y, x); + + p1.Offset(p2); + + Assert.Equal(unchecked(p2.X + p2.Y), p1.X); + Assert.Equal(p1.X, p1.Y); + + p2.Offset(x, y); + Assert.Equal(p1, p2); + } + + [Fact] + public void RotateTest() + { + var p = new Point(13, 17); + Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty); + + var pout = Point.Transform(p, matrix); + + Assert.Equal(new Point(-3, 21), pout); + } + + [Fact] + public void SkewTest() + { + var p = new Point(13, 17); + Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty); + + var pout = Point.Transform(p, matrix); + Assert.Equal(new Point(30, 30), pout); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void EqualityTest(int x, int y) + { + var p1 = new Point(x, y); + var p2 = new Point((x / 2) - 1, (y / 2) - 1); + var p3 = new Point(x, y); + + Assert.True(p1 == p3); + Assert.True(p1 != p2); + Assert.True(p2 != p3); + + Assert.True(p1.Equals(p3)); + Assert.False(p1.Equals(p2)); + Assert.False(p2.Equals(p3)); + + Assert.True(p1.Equals((object)p3)); + Assert.False(p1.Equals((object)p2)); + Assert.False(p2.Equals((object)p3)); + + Assert.Equal(p1.GetHashCode(), p3.GetHashCode()); + } + + [Fact] + public void EqualityTest_NotPoint() + { + var point = new Point(0, 0); + Assert.False(point.Equals(null)); + Assert.False(point.Equals(0)); + Assert.False(point.Equals(new PointF(0, 0))); + } + + [Fact] + public void GetHashCodeTest() + { + var point = new Point(10, 10); + Assert.Equal(point.GetHashCode(), new Point(10, 10).GetHashCode()); + Assert.NotEqual(point.GetHashCode(), new Point(20, 10).GetHashCode()); + Assert.NotEqual(point.GetHashCode(), new Point(10, 20).GetHashCode()); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(1, -2, 3, -4)] + public void ConversionTest(int x, int y, int width, int height) + { + var rect = new Rectangle(x, y, width, height); + RectangleF rectF = rect; + Assert.Equal(x, rectF.X); + Assert.Equal(y, rectF.Y); + Assert.Equal(width, rectF.Width); + Assert.Equal(height, rectF.Height); + } + + [Fact] + public void ToStringTest() + { + var p = new Point(5, -5); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Point [ X={0}, Y={1} ]", p.X, p.Y), p.ToString()); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void DeconstructTest(int x, int y) + { + Point p = new Point(x, y); + + (int deconstructedX, int deconstructedY) = p; + + Assert.Equal(x, deconstructedX); + Assert.Equal(y, deconstructedY); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs b/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs new file mode 100644 index 0000000000..f83f6435a2 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs @@ -0,0 +1,286 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using System.Reflection; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + /// + /// Tests the struct. + /// + public class RectangleFTests + { + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, RectangleF.Empty); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void NonDefaultConstructorTest(float x, float y, float width, float height) + { + var rect1 = new RectangleF(x, y, width, height); + var p = new PointF(x, y); + var s = new SizeF(width, height); + var rect2 = new RectangleF(p, s); + + Assert.Equal(rect1, rect2); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void FromLTRBTest(float left, float top, float right, float bottom) + { + var expected = new RectangleF(left, top, right - left, bottom - top); + var actual = RectangleF.FromLTRB(left, top, right, bottom); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void DimensionsTest(float x, float y, float width, float height) + { + var rect = new RectangleF(x, y, width, height); + var p = new PointF(x, y); + var s = new SizeF(width, height); + + Assert.Equal(p, rect.Location); + Assert.Equal(s, rect.Size); + Assert.Equal(x, rect.X); + Assert.Equal(y, rect.Y); + Assert.Equal(width, rect.Width); + Assert.Equal(height, rect.Height); + Assert.Equal(x, rect.Left); + Assert.Equal(y, rect.Top); + Assert.Equal(x + width, rect.Right); + Assert.Equal(y + height, rect.Bottom); + } + + [Fact] + public void IsEmptyTest() + { + Assert.True(RectangleF.Empty.IsEmpty); + Assert.True(default(RectangleF).IsEmpty); + Assert.True(new RectangleF(1, -2, -10, 10).IsEmpty); + Assert.True(new RectangleF(1, -2, 10, -10).IsEmpty); + Assert.True(new RectangleF(1, -2, 0, 0).IsEmpty); + + Assert.False(new RectangleF(0, 0, 10, 10).IsEmpty); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(float.MaxValue, float.MinValue)] + public void LocationSetTest(float x, float y) + { + var point = new PointF(x, y); + var rect = new RectangleF(10, 10, 10, 10) { Location = point }; + Assert.Equal(point, rect.Location); + Assert.Equal(point.X, rect.X); + Assert.Equal(point.Y, rect.Y); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(float.MaxValue, float.MinValue)] + public void SizeSetTest(float x, float y) + { + var size = new SizeF(x, y); + var rect = new RectangleF(10, 10, 10, 10) { Size = size }; + Assert.Equal(size, rect.Size); + Assert.Equal(size.Width, rect.Width); + Assert.Equal(size.Height, rect.Height); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void EqualityTest(float x, float y, float width, float height) + { + var rect1 = new RectangleF(x, y, width, height); + var rect2 = new RectangleF(width, height, x, y); + + Assert.True(rect1 != rect2); + Assert.False(rect1 == rect2); + Assert.False(rect1.Equals(rect2)); + Assert.False(rect1.Equals((object)rect2)); + } + + [Fact] + public void EqualityTestNotRectangleF() + { + var rectangle = new RectangleF(0, 0, 0, 0); + Assert.False(rectangle.Equals(null)); + Assert.False(rectangle.Equals(0)); + + // If RectangleF implements IEquatable (e.g. in .NET Core), then classes that are implicitly + // convertible to RectangleF can potentially be equal. + // See https://github.com/dotnet/corefx/issues/5255. + bool expectsImplicitCastToRectangleF = typeof(IEquatable).IsAssignableFrom(rectangle.GetType()); + Assert.Equal(expectsImplicitCastToRectangleF, rectangle.Equals(new Rectangle(0, 0, 0, 0))); + + Assert.False(rectangle.Equals((object)new Rectangle(0, 0, 0, 0))); // No implicit cast + } + + [Fact] + public void GetHashCodeTest() + { + var rect1 = new RectangleF(10, 10, 10, 10); + var rect2 = new RectangleF(10, 10, 10, 10); + Assert.Equal(rect1.GetHashCode(), rect2.GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new RectangleF(20, 10, 10, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 20, 10, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 10, 20, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 10, 10, 20).GetHashCode()); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void ContainsTest(float x, float y, float width, float height) + { + var rect = new RectangleF(x, y, width, height); + float x1 = (x + width) / 2; + float y1 = (y + height) / 2; + var p = new PointF(x1, y1); + var r = new RectangleF(x1, y1, width / 2, height / 2); + + Assert.False(rect.Contains(x1, y1)); + Assert.False(rect.Contains(p)); + Assert.False(rect.Contains(r)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue / 2, float.MinValue / 2, float.MinValue / 2, float.MaxValue / 2)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void InflateTest(float x, float y, float width, float height) + { + var rect = new RectangleF(x, y, width, height); + var inflatedRect = new RectangleF(x - width, y - height, width + (2 * width), height + (2 * height)); + + rect.Inflate(width, height); + Assert.Equal(inflatedRect, rect); + + var s = new SizeF(x, y); + inflatedRect = RectangleF.Inflate(rect, x, y); + + rect.Inflate(s); + Assert.Equal(inflatedRect, rect); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue, float.MaxValue / 2, float.MinValue / 2)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void IntersectTest(float x, float y, float width, float height) + { + var rect1 = new RectangleF(x, y, width, height); + var rect2 = new RectangleF(y, x, width, height); + var expectedRect = RectangleF.Intersect(rect1, rect2); + rect1.Intersect(rect2); + Assert.Equal(expectedRect, rect1); + Assert.False(rect1.IntersectsWith(expectedRect)); + } + + [Fact] + public void IntersectIntersectingRectsTest() + { + var rect1 = new RectangleF(0, 0, 5, 5); + var rect2 = new RectangleF(1, 1, 3, 3); + var expected = new RectangleF(1, 1, 3, 3); + + Assert.Equal(expected, RectangleF.Intersect(rect1, rect2)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void UnionTest(float x, float y, float width, float height) + { + var a = new RectangleF(x, y, width, height); + var b = new RectangleF(width, height, x, y); + + float x1 = Math.Min(a.X, b.X); + float x2 = Math.Max(a.X + a.Width, b.X + b.Width); + float y1 = Math.Min(a.Y, b.Y); + float y2 = Math.Max(a.Y + a.Height, b.Y + b.Height); + + var expectedRectangle = new RectangleF(x1, y1, x2 - x1, y2 - y1); + + Assert.Equal(expectedRectangle, RectangleF.Union(a, b)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, 0, 0, float.MaxValue)] + [InlineData(0, float.MinValue, float.MaxValue, 0)] + public void OffsetTest(float x, float y, float width, float height) + { + var r1 = new RectangleF(x, y, width, height); + var expectedRect = new RectangleF(x + width, y + height, width, height); + var p = new PointF(width, height); + + r1.Offset(p); + Assert.Equal(expectedRect, r1); + + expectedRect.Offset(p); + r1.Offset(width, height); + Assert.Equal(expectedRect, r1); + } + + [Fact] + public void ToStringTest() + { + var r = new RectangleF(5, 5.1F, 1.3F, 1); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "RectangleF [ X={0}, Y={1}, Width={2}, Height={3} ]", r.X, r.Y, r.Width, r.Height), r.ToString()); + } + + [Theory] + [InlineData(float.MinValue, float.MaxValue, float.MaxValue, float.MaxValue)] + [InlineData(float.MinValue, float.MaxValue, float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MaxValue)] + [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue, float.MaxValue, float.MaxValue)] + [InlineData(float.MinValue, float.MinValue, float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MinValue, float.MinValue, float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue)] + [InlineData(float.MaxValue, float.MaxValue, float.MaxValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, float.MaxValue, float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MaxValue)] + [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MinValue)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)] + [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MinValue)] + [InlineData(0, 0, 0, 0)] + public void DeconstructTest(float x, float y, float width, float height) + { + RectangleF r = new RectangleF(x, y, width, height); + + (float dx, float dy, float dw, float dh) = r; + + Assert.Equal(x, dx); + Assert.Equal(y, dy); + Assert.Equal(width, dw); + Assert.Equal(height, dh); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs b/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs new file mode 100644 index 0000000000..84b15d36e0 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs @@ -0,0 +1,336 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + /// + /// Tests the struct. + /// + public class RectangleTests + { + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, Rectangle.Empty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, 0, int.MinValue, 0)] + [InlineData(0, 0, 0, 0)] + [InlineData(0, int.MinValue, 0, int.MaxValue)] + public void NonDefaultConstructorTest(int x, int y, int width, int height) + { + var rect1 = new Rectangle(x, y, width, height); + var rect2 = new Rectangle(new Point(x, y), new Size(width, height)); + + Assert.Equal(rect1, rect2); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, 0, int.MinValue, 0)] + [InlineData(0, 0, 0, 0)] + [InlineData(0, int.MinValue, 0, int.MaxValue)] + public void FromLTRBTest(int left, int top, int right, int bottom) + { + var rect1 = new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top)); + var rect2 = Rectangle.FromLTRB(left, top, right, bottom); + + Assert.Equal(rect1, rect2); + } + + [Fact] + public void EmptyTest() + { + Assert.True(Rectangle.Empty.IsEmpty); + Assert.True(default(Rectangle).IsEmpty); + Assert.True(new Rectangle(0, 0, 0, 0).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, 0, int.MinValue, 0)] + [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] + [InlineData(0, int.MinValue, 0, int.MaxValue)] + public void NonEmptyTest(int x, int y, int width, int height) + { + Assert.False(new Rectangle(x, y, width, height).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, 0, int.MinValue, 0)] + [InlineData(0, 0, 0, 0)] + [InlineData(0, int.MinValue, 0, int.MaxValue)] + [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] + public void DimensionsTest(int x, int y, int width, int height) + { + var rect = new Rectangle(x, y, width, height); + Assert.Equal(new Point(x, y), rect.Location); + Assert.Equal(new Size(width, height), rect.Size); + + Assert.Equal(x, rect.X); + Assert.Equal(y, rect.Y); + Assert.Equal(width, rect.Width); + Assert.Equal(height, rect.Height); + Assert.Equal(x, rect.Left); + Assert.Equal(y, rect.Top); + Assert.Equal(unchecked(x + width), rect.Right); + Assert.Equal(unchecked(y + height), rect.Bottom); + + var p = new Point(width, height); + var s = new Size(x, y); + rect.Location = p; + rect.Size = s; + + Assert.Equal(p, rect.Location); + Assert.Equal(s, rect.Size); + + Assert.Equal(width, rect.X); + Assert.Equal(height, rect.Y); + Assert.Equal(x, rect.Width); + Assert.Equal(y, rect.Height); + Assert.Equal(width, rect.Left); + Assert.Equal(height, rect.Top); + Assert.Equal(unchecked(x + width), rect.Right); + Assert.Equal(unchecked(y + height), rect.Bottom); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(int.MaxValue, int.MinValue)] + public void LocationSetTest(int x, int y) + { + var point = new Point(x, y); + var rect = new Rectangle(10, 10, 10, 10) { Location = point }; + Assert.Equal(point, rect.Location); + Assert.Equal(point.X, rect.X); + Assert.Equal(point.Y, rect.Y); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(int.MaxValue, int.MinValue)] + public void SizeSetTest(int x, int y) + { + var size = new Size(x, y); + var rect = new Rectangle(10, 10, 10, 10) { Size = size }; + Assert.Equal(size, rect.Size); + Assert.Equal(size.Width, rect.Width); + Assert.Equal(size.Height, rect.Height); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, 0, int.MinValue, 0)] + [InlineData(0, int.MinValue, 0, int.MaxValue)] + [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] + public void EqualityTest(int x, int y, int width, int height) + { + var rect1 = new Rectangle(x, y, width, height); + var rect2 = new Rectangle(width / 2, height / 2, x, y); + + Assert.True(rect1 != rect2); + Assert.False(rect1 == rect2); + Assert.False(rect1.Equals(rect2)); + Assert.False(rect1.Equals((object)rect2)); + } + + [Fact] + public void EqualityTestNotRectangle() + { + var rectangle = new Rectangle(0, 0, 0, 0); + Assert.False(rectangle.Equals(null)); + Assert.False(rectangle.Equals(0)); + Assert.False(rectangle.Equals(new RectangleF(0, 0, 0, 0))); + } + + [Fact] + public void GetHashCodeTest() + { + var rect1 = new Rectangle(10, 10, 10, 10); + var rect2 = new Rectangle(10, 10, 10, 10); + Assert.Equal(rect1.GetHashCode(), rect2.GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new Rectangle(20, 10, 10, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 20, 10, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 10, 20, 10).GetHashCode()); + Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 10, 10, 20).GetHashCode()); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MaxValue)] + [InlineData(0, 0, 0, 0)] + public void RectangleFConversionTest(float x, float y, float width, float height) + { + var rect = new RectangleF(x, y, width, height); + Rectangle rCeiling, rTruncate, rRound; + + unchecked + { + rCeiling = new Rectangle( + (int)Math.Ceiling(x), + (int)Math.Ceiling(y), + (int)Math.Ceiling(width), + (int)Math.Ceiling(height)); + + rTruncate = new Rectangle((int)x, (int)y, (int)width, (int)height); + + rRound = new Rectangle( + (int)Math.Round(x), + (int)Math.Round(y), + (int)Math.Round(width), + (int)Math.Round(height)); + } + + Assert.Equal(rCeiling, Rectangle.Ceiling(rect)); + Assert.Equal(rTruncate, Rectangle.Truncate(rect)); + Assert.Equal(rRound, Rectangle.Round(rect)); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(0, int.MinValue, int.MaxValue, 0)] + public void ContainsTest(int x, int y, int width, int height) + { + var rect = new Rectangle(unchecked((2 * x) - width), unchecked((2 * y) - height), width, height); + var p = new Point(x, y); + var r = new Rectangle(x, y, width / 2, height / 2); + + Assert.False(rect.Contains(x, y)); + Assert.False(rect.Contains(p)); + Assert.False(rect.Contains(r)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(0, int.MinValue, int.MaxValue, 0)] + public void InflateTest(int x, int y, int width, int height) + { + Rectangle inflatedRect, rect = new Rectangle(x, y, width, height); + unchecked + { + inflatedRect = new Rectangle(x - width, y - height, width + (2 * width), height + (2 * height)); + } + + Assert.Equal(inflatedRect, Rectangle.Inflate(rect, width, height)); + + rect.Inflate(width, height); + Assert.Equal(inflatedRect, rect); + + var s = new Size(x, y); + unchecked + { + inflatedRect = new Rectangle(rect.X - x, rect.Y - y, rect.Width + (2 * x), rect.Height + (2 * y)); + } + + rect.Inflate(s); + Assert.Equal(inflatedRect, rect); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(0, int.MinValue, int.MaxValue, 0)] + public void IntersectTest(int x, int y, int width, int height) + { + var rect = new Rectangle(x, y, width, height); + var expectedRect = Rectangle.Intersect(rect, rect); + rect.Intersect(rect); + Assert.Equal(expectedRect, rect); + Assert.False(rect.IntersectsWith(expectedRect)); + } + + [Fact] + public void IntersectIntersectingRectsTest() + { + var rect1 = new Rectangle(0, 0, 5, 5); + var rect2 = new Rectangle(1, 1, 3, 3); + var expected = new Rectangle(1, 1, 3, 3); + + Assert.Equal(expected, Rectangle.Intersect(rect1, rect2)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(int.MaxValue, 0, 0, int.MaxValue)] + [InlineData(0, int.MinValue, int.MaxValue, 0)] + public void UnionTest(int x, int y, int width, int height) + { + var a = new Rectangle(x, y, width, height); + var b = new Rectangle(width, height, x, y); + + int x1 = Math.Min(a.X, b.X); + int x2 = Math.Max(a.X + a.Width, b.X + b.Width); + int y1 = Math.Min(a.Y, b.Y); + int y2 = Math.Max(a.Y + a.Height, b.Y + b.Height); + + var expectedRectangle = new Rectangle(x1, y1, x2 - x1, y2 - y1); + + Assert.Equal(expectedRectangle, Rectangle.Union(a, b)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(int.MaxValue, 0, 0, int.MaxValue)] + [InlineData(0, int.MinValue, int.MaxValue, 0)] + public void OffsetTest(int x, int y, int width, int height) + { + var r1 = new Rectangle(x, y, width, height); + var expectedRect = new Rectangle(x + width, y + height, width, height); + var p = new Point(width, height); + + r1.Offset(p); + Assert.Equal(expectedRect, r1); + + expectedRect.Offset(p); + r1.Offset(width, height); + Assert.Equal(expectedRect, r1); + } + + [Fact] + public void ToStringTest() + { + var r = new Rectangle(5, -5, 0, 1); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Rectangle [ X={0}, Y={1}, Width={2}, Height={3} ]", r.X, r.Y, r.Width, r.Height), r.ToString()); + } + + [Theory] + [InlineData(int.MinValue, int.MaxValue, int.MaxValue, int.MaxValue)] + [InlineData(int.MinValue, int.MaxValue, int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)] + [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MaxValue)] + [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(int.MinValue, int.MinValue, int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue)] + [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue, int.MinValue, int.MaxValue)] + [InlineData(int.MaxValue, int.MaxValue, int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MaxValue)] + [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)] + [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MinValue)] + [InlineData(0, 0, 0, 0)] + public void DeconstructTest(int x, int y, int width, int height) + { + var r = new Rectangle(x, y, width, height); + + (int dx, int dy, int dw, int dh) = r; + + Assert.Equal(x, dx); + Assert.Equal(y, dy); + Assert.Equal(width, dw); + Assert.Equal(height, dh); + } + } +} diff --git a/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs b/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs new file mode 100644 index 0000000000..5bfc8f5ef1 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs @@ -0,0 +1,250 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using System.Reflection; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + public class SizeFTests + { + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, SizeF.Empty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void NonDefaultConstructorAndDimensionsTest(float width, float height) + { + var s1 = new SizeF(width, height); + var p1 = new PointF(width, height); + var s2 = new SizeF(s1); + + Assert.Equal(s1, s2); + Assert.Equal(s1, new SizeF(p1)); + Assert.Equal(s2, new SizeF(p1)); + + Assert.Equal(width, s1.Width); + Assert.Equal(height, s1.Height); + + s1.Width = 10; + Assert.Equal(10, s1.Width); + + s1.Height = -10.123f; + Assert.Equal(-10.123, s1.Height, 3); + } + + [Fact] + public void IsEmptyDefaultsTest() + { + Assert.True(SizeF.Empty.IsEmpty); + Assert.True(default(SizeF).IsEmpty); + Assert.True(new SizeF(0, 0).IsEmpty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + public void IsEmptyRandomTest(float width, float height) + { + Assert.False(new SizeF(width, height).IsEmpty); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void ArithmeticTest(float width, float height) + { + var s1 = new SizeF(width, height); + var s2 = new SizeF(height, width); + var addExpected = new SizeF(width + height, width + height); + var subExpected = new SizeF(width - height, height - width); + + Assert.Equal(addExpected, s1 + s2); + Assert.Equal(addExpected, SizeF.Add(s1, s2)); + + Assert.Equal(subExpected, s1 - s2); + Assert.Equal(subExpected, SizeF.Subtract(s1, s2)); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void EqualityTest(float width, float height) + { + var sLeft = new SizeF(width, height); + var sRight = new SizeF(height, width); + + if (width == height) + { + Assert.True(sLeft == sRight); + Assert.False(sLeft != sRight); + Assert.True(sLeft.Equals(sRight)); + Assert.True(sLeft.Equals((object)sRight)); + Assert.Equal(sLeft.GetHashCode(), sRight.GetHashCode()); + return; + } + + Assert.True(sLeft != sRight); + Assert.False(sLeft == sRight); + Assert.False(sLeft.Equals(sRight)); + Assert.False(sLeft.Equals((object)sRight)); + } + + [Fact] + public void EqualityTest_NotSizeF() + { + var size = new SizeF(0, 0); + Assert.False(size.Equals(null)); + Assert.False(size.Equals(0)); + + // If SizeF implements IEquatable (e.g in .NET Core), then classes that are implicitly + // convertible to SizeF can potentially be equal. + // See https://github.com/dotnet/corefx/issues/5255. + bool expectsImplicitCastToSizeF = typeof(IEquatable).IsAssignableFrom(size.GetType()); + Assert.Equal(expectsImplicitCastToSizeF, size.Equals(new Size(0, 0))); + + Assert.False(size.Equals((object)new Size(0, 0))); // No implicit cast + } + + [Fact] + public void GetHashCodeTest() + { + var size = new SizeF(10, 10); + Assert.Equal(size.GetHashCode(), new SizeF(10, 10).GetHashCode()); + Assert.NotEqual(size.GetHashCode(), new SizeF(20, 10).GetHashCode()); + Assert.NotEqual(size.GetHashCode(), new SizeF(10, 20).GetHashCode()); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void ConversionTest(float width, float height) + { + var s1 = new SizeF(width, height); + var p1 = (PointF)s1; + var s2 = new Size(unchecked((int)width), unchecked((int)height)); + + Assert.Equal(new PointF(width, height), p1); + Assert.Equal(p1, (PointF)s1); + Assert.Equal(s2, (Size)s1); + } + + [Fact] + public void ToStringTest() + { + var sz = new SizeF(10, 5); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "SizeF [ Width={0}, Height={1} ]", sz.Width, sz.Height), sz.ToString()); + } + + [Theory] + [InlineData(1000.234f, 0.0f)] + [InlineData(1000.234f, 1.0f)] + [InlineData(1000.234f, 2400.933f)] + [InlineData(1000.234f, float.MaxValue)] + [InlineData(1000.234f, -1.0f)] + [InlineData(1000.234f, -2400.933f)] + [InlineData(1000.234f, float.MinValue)] + [InlineData(float.MaxValue, 0.0f)] + [InlineData(float.MaxValue, 1.0f)] + [InlineData(float.MaxValue, 2400.933f)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(float.MaxValue, -1.0f)] + [InlineData(float.MaxValue, -2400.933f)] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, 0.0f)] + [InlineData(float.MinValue, 1.0f)] + [InlineData(float.MinValue, 2400.933f)] + [InlineData(float.MinValue, float.MaxValue)] + [InlineData(float.MinValue, -1.0f)] + [InlineData(float.MinValue, -2400.933f)] + [InlineData(float.MinValue, float.MinValue)] + public void MultiplicationTest(float dimension, float multiplier) + { + SizeF sz1 = new SizeF(dimension, dimension); + SizeF mulExpected; + + mulExpected = new SizeF(dimension * multiplier, dimension * multiplier); + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Theory] + [InlineData(1111.1111f, 2222.2222f, 3333.3333f)] + public void MultiplicationTestWidthHeightMultiplier(float width, float height, float multiplier) + { + SizeF sz1 = new SizeF(width, height); + SizeF mulExpected; + + mulExpected = new SizeF(width * multiplier, height * multiplier); + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Theory] + [InlineData(0.0f, 1.0f)] + [InlineData(1.0f, 1.0f)] + [InlineData(-1.0f, 1.0f)] + [InlineData(1.0f, -1.0f)] + [InlineData(-1.0f, -1.0f)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MaxValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, 1.0f)] + [InlineData(float.MinValue, 1.0f)] + [InlineData(float.MaxValue, -1.0f)] + [InlineData(float.MinValue, -1.0f)] + [InlineData(float.MinValue, 0.0f)] + [InlineData(1.0f, float.MinValue)] + [InlineData(1.0f, float.MaxValue)] + [InlineData(-1.0f, float.MinValue)] + [InlineData(-1.0f, float.MaxValue)] + public void DivideTestSizeFloat(float dimension, float divisor) + { + SizeF size = new SizeF(dimension, dimension); + SizeF expected = new SizeF(dimension / divisor, dimension / divisor); + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(-111.111f, 222.222f, 333.333f)] + public void DivideTestSizeFloatWidthHeightDivisor(float width, float height, float divisor) + { + SizeF size = new SizeF(width, height); + SizeF expected = new SizeF(width / divisor, height / divisor); + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void DeconstructTest(float width, float height) + { + SizeF s = new SizeF(width, height); + + (float deconstructedWidth, float deconstructedHeight) = s; + + Assert.Equal(width, deconstructedWidth); + Assert.Equal(height, deconstructedHeight); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs b/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs new file mode 100644 index 0000000000..af4b8430d1 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs @@ -0,0 +1,379 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Globalization; +using Xunit; + +namespace SixLabors.Primitives.Tests +{ + /// + /// Tests the struct. + /// + public class SizeTests + { + [Fact] + public void DefaultConstructorTest() + { + Assert.Equal(default, Size.Empty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void NonDefaultConstructorTest(int width, int height) + { + var s1 = new Size(width, height); + var s2 = new Size(new Point(width, height)); + + Assert.Equal(s1, s2); + + s1.Width = 10; + Assert.Equal(10, s1.Width); + + s1.Height = -10; + Assert.Equal(-10, s1.Height); + } + + [Fact] + public void IsEmptyDefaultsTest() + { + Assert.True(Size.Empty.IsEmpty); + Assert.True(default(Size).IsEmpty); + Assert.True(new Size(0, 0).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + public void IsEmptyRandomTest(int width, int height) + { + Assert.False(new Size(width, height).IsEmpty); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void DimensionsTest(int width, int height) + { + var p = new Size(width, height); + Assert.Equal(width, p.Width); + Assert.Equal(height, p.Height); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void PointFConversionTest(int width, int height) + { + SizeF sz = new Size(width, height); + Assert.Equal(new SizeF(width, height), sz); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void SizeConversionTest(int width, int height) + { + var sz = (Point)new Size(width, height); + Assert.Equal(new Point(width, height), sz); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void ArithmeticTest(int width, int height) + { + var sz1 = new Size(width, height); + var sz2 = new Size(height, width); + Size addExpected, subExpected; + + unchecked + { + addExpected = new Size(width + height, height + width); + subExpected = new Size(width - height, height - width); + } + + Assert.Equal(addExpected, sz1 + sz2); + Assert.Equal(subExpected, sz1 - sz2); + Assert.Equal(addExpected, Size.Add(sz1, sz2)); + Assert.Equal(subExpected, Size.Subtract(sz1, sz2)); + } + + [Theory] + [InlineData(float.MaxValue, float.MinValue)] + [InlineData(float.MinValue, float.MinValue)] + [InlineData(float.MaxValue, float.MaxValue)] + [InlineData(0, 0)] + public void PointFMathematicalTest(float width, float height) + { + var szF = new SizeF(width, height); + Size pCeiling, pTruncate, pRound; + + unchecked + { + pCeiling = new Size((int)MathF.Ceiling(width), (int)MathF.Ceiling(height)); + pTruncate = new Size((int)width, (int)height); + pRound = new Size((int)MathF.Round(width), (int)MathF.Round(height)); + } + + Assert.Equal(pCeiling, Size.Ceiling(szF)); + Assert.Equal(pRound, Size.Round(szF)); + Assert.Equal(pTruncate, (Size)szF); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void EqualityTest(int width, int height) + { + var p1 = new Size(width, height); + var p2 = new Size(unchecked(width - 1), unchecked(height - 1)); + var p3 = new Size(width, height); + + Assert.True(p1 == p3); + Assert.True(p1 != p2); + Assert.True(p2 != p3); + + Assert.True(p1.Equals(p3)); + Assert.False(p1.Equals(p2)); + Assert.False(p2.Equals(p3)); + + Assert.True(p1.Equals((object)p3)); + Assert.False(p1.Equals((object)p2)); + Assert.False(p2.Equals((object)p3)); + + Assert.Equal(p1.GetHashCode(), p3.GetHashCode()); + } + + [Fact] + public void EqualityTest_NotSize() + { + var size = new Size(0, 0); + Assert.False(size.Equals(null)); + Assert.False(size.Equals(0)); + Assert.False(size.Equals(new SizeF(0, 0))); + } + + [Fact] + public void GetHashCodeTest() + { + var size = new Size(10, 10); + Assert.Equal(size.GetHashCode(), new Size(10, 10).GetHashCode()); + Assert.NotEqual(size.GetHashCode(), new Size(20, 10).GetHashCode()); + Assert.NotEqual(size.GetHashCode(), new Size(10, 20).GetHashCode()); + } + + [Fact] + public void ToStringTest() + { + var sz = new Size(10, 5); + Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Size [ Width={0}, Height={1} ]", sz.Width, sz.Height), sz.ToString()); + } + + [Theory] + [InlineData(1000, 0)] + [InlineData(1000, 1)] + [InlineData(1000, 2400)] + [InlineData(1000, int.MaxValue)] + [InlineData(1000, -1)] + [InlineData(1000, -2400)] + [InlineData(1000, int.MinValue)] + [InlineData(int.MaxValue, 0)] + [InlineData(int.MaxValue, 1)] + [InlineData(int.MaxValue, 2400)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(int.MaxValue, -1)] + [InlineData(int.MaxValue, -2400)] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, 0)] + [InlineData(int.MinValue, 1)] + [InlineData(int.MinValue, 2400)] + [InlineData(int.MinValue, int.MaxValue)] + [InlineData(int.MinValue, -1)] + [InlineData(int.MinValue, -2400)] + [InlineData(int.MinValue, int.MinValue)] + public void MultiplicationTestSizeInt(int dimension, int multiplier) + { + Size sz1 = new Size(dimension, dimension); + Size mulExpected; + + unchecked + { + mulExpected = new Size(dimension * multiplier, dimension * multiplier); + } + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Theory] + [InlineData(1000, 2000, 3000)] + public void MultiplicationTestSizeIntWidthHeightMultiplier(int width, int height, int multiplier) + { + Size sz1 = new Size(width, height); + Size mulExpected; + + unchecked + { + mulExpected = new Size(width * multiplier, height * multiplier); + } + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Theory] + [InlineData(1000, 0.0f)] + [InlineData(1000, 1.0f)] + [InlineData(1000, 2400.933f)] + [InlineData(1000, float.MaxValue)] + [InlineData(1000, -1.0f)] + [InlineData(1000, -2400.933f)] + [InlineData(1000, float.MinValue)] + [InlineData(int.MaxValue, 0.0f)] + [InlineData(int.MaxValue, 1.0f)] + [InlineData(int.MaxValue, 2400.933f)] + [InlineData(int.MaxValue, float.MaxValue)] + [InlineData(int.MaxValue, -1.0f)] + [InlineData(int.MaxValue, -2400.933f)] + [InlineData(int.MaxValue, float.MinValue)] + [InlineData(int.MinValue, 0.0f)] + [InlineData(int.MinValue, 1.0f)] + [InlineData(int.MinValue, 2400.933f)] + [InlineData(int.MinValue, float.MaxValue)] + [InlineData(int.MinValue, -1.0f)] + [InlineData(int.MinValue, -2400.933f)] + [InlineData(int.MinValue, float.MinValue)] + public void MultiplicationTestSizeFloat(int dimension, float multiplier) + { + Size sz1 = new Size(dimension, dimension); + SizeF mulExpected; + + mulExpected = new SizeF(dimension * multiplier, dimension * multiplier); + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Theory] + [InlineData(1000, 2000, 30.33f)] + public void MultiplicationTestSizeFloatWidthHeightMultiplier(int width, int height, float multiplier) + { + Size sz1 = new Size(width, height); + SizeF mulExpected; + + mulExpected = new SizeF(width * multiplier, height * multiplier); + + Assert.Equal(mulExpected, sz1 * multiplier); + Assert.Equal(mulExpected, multiplier * sz1); + } + + [Fact] + public void DivideByZeroChecks() + { + Size size = new Size(100, 100); + Assert.Throws(() => size / 0); + + SizeF expectedSizeF = new SizeF(float.PositiveInfinity, float.PositiveInfinity); + Assert.Equal(expectedSizeF, size / 0.0f); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 1)] + [InlineData(-1, 1)] + [InlineData(1, -1)] + [InlineData(-1, -1)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MaxValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, 1)] + [InlineData(int.MinValue, 1)] + [InlineData(int.MaxValue, -1)] + public void DivideTestSizeInt(int dimension, int divisor) + { + Size size = new Size(dimension, dimension); + Size expected; + + expected = new Size(dimension / divisor, dimension / divisor); + + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(1111, 2222, 3333)] + public void DivideTestSizeIntWidthHeightDivisor(int width, int height, int divisor) + { + Size size = new Size(width, height); + Size expected; + + expected = new Size(width / divisor, height / divisor); + + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(0, 1.0f)] + [InlineData(1, 1.0f)] + [InlineData(-1, 1.0f)] + [InlineData(1, -1.0f)] + [InlineData(-1, -1.0f)] + [InlineData(int.MaxValue, float.MaxValue)] + [InlineData(int.MaxValue, float.MinValue)] + [InlineData(int.MinValue, float.MaxValue)] + [InlineData(int.MinValue, float.MinValue)] + [InlineData(int.MaxValue, 1.0f)] + [InlineData(int.MinValue, 1.0f)] + [InlineData(int.MaxValue, -1.0f)] + [InlineData(int.MinValue, -1.0f)] + public void DivideTestSizeFloat(int dimension, float divisor) + { + SizeF size = new SizeF(dimension, dimension); + SizeF expected; + + expected = new SizeF(dimension / divisor, dimension / divisor); + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(1111, 2222, -333.33f)] + public void DivideTestSizeFloatWidthHeightDivisor(int width, int height, float divisor) + { + SizeF size = new SizeF(width, height); + SizeF expected; + + expected = new SizeF(width / divisor, height / divisor); + Assert.Equal(expected, size / divisor); + } + + [Theory] + [InlineData(int.MaxValue, int.MinValue)] + [InlineData(int.MinValue, int.MinValue)] + [InlineData(int.MaxValue, int.MaxValue)] + [InlineData(0, 0)] + public void DeconstructTest(int width, int height) + { + Size s = new Size(width, height); + + (int deconstructedWidth, int deconstructedHeight) = s; + + Assert.Equal(width, deconstructedWidth); + Assert.Equal(height, deconstructedHeight); + } + } +} \ No newline at end of file