From f4626c1cebc534cd2eb28102e79abb63728c32aa Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 3 Jun 2017 10:15:40 +0100 Subject: [PATCH] [SL.Core] add matrix tests & fix GetHashCode --- src/Shared/AssemblyInfo.Common.cs | 9 +- .../ApproximateFloatComparer.cs | 210 ++++ src/SixLabors.Primitives/Ellipse.cs | 11 +- src/SixLabors.Primitives/HashHelpers.cs | 23 + src/SixLabors.Primitives/LongRational.cs | 5 +- src/SixLabors.Primitives/MathF.cs | 4 +- src/SixLabors.Primitives/Matrix.cs | 51 +- src/SixLabors.Primitives/Point.cs | 25 +- src/SixLabors.Primitives/PointF.cs | 65 +- src/SixLabors.Primitives/Rectangle.cs | 26 +- src/SixLabors.Primitives/RectangleF.cs | 26 +- src/SixLabors.Primitives/Size.cs | 21 +- src/SixLabors.Primitives/SizeF.cs | 20 +- tests/CodeCoverage/.gitignore | 1 + tests/CodeCoverage/CodeCoverage.cmd | 2 +- .../SixLabors.Primitives.Tests/MatrixTests.cs | 1015 +++++++++++++++++ .../SixLabors.Primitives.Tests/PointFTests.cs | 2 +- .../SixLabors.Primitives.Tests.csproj | 3 +- 18 files changed, 1470 insertions(+), 49 deletions(-) create mode 100644 src/SixLabors.Primitives/ApproximateFloatComparer.cs create mode 100644 src/SixLabors.Primitives/HashHelpers.cs create mode 100644 tests/CodeCoverage/.gitignore create mode 100644 tests/SixLabors.Primitives.Tests/MatrixTests.cs diff --git a/src/Shared/AssemblyInfo.Common.cs b/src/Shared/AssemblyInfo.Common.cs index 081b01994..b140dd917 100644 --- a/src/Shared/AssemblyInfo.Common.cs +++ b/src/Shared/AssemblyInfo.Common.cs @@ -10,11 +10,11 @@ using System.Runtime.CompilerServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyDescription("A cross-platform library for processing of image files; written in C#")] +[assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Scott Williams")] -[assembly: AssemblyProduct("SixLabors.Shapes")] -[assembly: AssemblyCopyright("Copyright (c) Scott Williams and contributors.")] +[assembly: AssemblyProduct("SixLabors.Primitives")] +[assembly: AssemblyCopyright("Copyright (c) Six Labors and contributors.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] @@ -34,5 +34,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyInformationalVersion("1.0.0.0")] // Ensure the internals can be tested. -[assembly: InternalsVisibleTo("SixLabors.Shapes.Tests")] -[assembly: InternalsVisibleTo("SixLabors.Shapes.Benchmarks")] +[assembly: InternalsVisibleTo("SixLabors.Primitives.Tests")] diff --git a/src/SixLabors.Primitives/ApproximateFloatComparer.cs b/src/SixLabors.Primitives/ApproximateFloatComparer.cs new file mode 100644 index 000000000..0d854a110 --- /dev/null +++ b/src/SixLabors.Primitives/ApproximateFloatComparer.cs @@ -0,0 +1,210 @@ +// +// 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.Primitives +{ + internal struct ApproximateFloatComparer + : IEqualityComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer, + IEqualityComparer + + { + private readonly float tolerance; + const float defaultTolerance = 1e-5f; + + public ApproximateFloatComparer(float tolerance = defaultTolerance) + { + this.tolerance = tolerance; + } + + public static bool Equal(float x, float y, float tolerance) + { + float d = x - y; + + return d > -tolerance && d < tolerance; + } + + public static bool Equal(float x, float y) + { + return Equal(x, y, defaultTolerance); + } + + public bool Equals(float x, float y) + { + return Equal(x, y, this.tolerance); + } + + public int GetHashCode(float obj) + { + var diff = obj % this.tolerance;// how different from tollerance are we? + return (obj - diff).GetHashCode(); + } + + public bool Equals(Vector4 a, Vector4 b) + { + return this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W); + } + + public int GetHashCode(Vector4 obj) + { + int hash = GetHashCode(obj.X); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Y)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Z)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.W)); + return hash; + } + + public bool Equals(Vector2 a, Vector2 b) + { + return this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y); + } + + public int GetHashCode(Vector2 obj) + { + int hash = GetHashCode(obj.X); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Y)); + return hash; + } + + public bool Equals(Vector3 a, Vector3 b) + { + return this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z); + } + + public int GetHashCode(Vector3 obj) + { + int hash = GetHashCode(obj.X); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Y)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Z)); + return hash; + } + + public static bool Equal(Matrix3x2 a, Matrix3x2 b, float tolerance) + { + return Equal(a.M11, b.M11, tolerance) && + Equal(a.M12, b.M12, tolerance) && + Equal(a.M21, b.M21, tolerance) && + Equal(a.M22, b.M22, tolerance) && + Equal(a.M31, b.M31, tolerance) && + Equal(a.M32, b.M32, tolerance); + } + + public static bool Equal(Matrix3x2 a, Matrix3x2 b) + { + return Equal(a, b, defaultTolerance); + } + + public bool Equals(Matrix3x2 a, Matrix3x2 b) + { + return Equal(a, b, this.tolerance); + } + + public int GetHashCode(Matrix3x2 obj) + { + int hash = GetHashCode(obj.M11); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M11)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M12)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M21)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M22)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M31)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M32)); + return hash; + } + + + public static bool Equal(Matrix4x4 a, Matrix4x4 b, float tolerance) + { + return + Equal(a.M11, b.M11, tolerance) && + Equal(a.M12, b.M12, tolerance) && + Equal(a.M13, b.M13, tolerance) && + Equal(a.M14, b.M14, tolerance) && + + Equal(a.M21, b.M21, tolerance) && + Equal(a.M22, b.M22, tolerance) && + Equal(a.M23, b.M23, tolerance) && + Equal(a.M24, b.M24, tolerance) && + + Equal(a.M31, b.M31, tolerance) && + Equal(a.M32, b.M32, tolerance) && + Equal(a.M33, b.M33, tolerance) && + Equal(a.M34, b.M34, tolerance) && + + Equal(a.M41, b.M41, tolerance) && + Equal(a.M42, b.M42, tolerance) && + Equal(a.M43, b.M43, tolerance) && + Equal(a.M44, b.M44, tolerance); + } + + public static bool Equal(Matrix4x4 a, Matrix4x4 b) + { + return Equal(a, b, defaultTolerance); + } + + public bool Equals(Matrix4x4 a, Matrix4x4 b) + { + return Equal(a, b, this.tolerance); + } + + + public int GetHashCode(Matrix4x4 obj) + { + int hash = GetHashCode(obj.M11); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M12)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M13)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M14)); + + hash = HashHelpers.Combine(hash, GetHashCode(obj.M21)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M22)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M23)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M24)); + + hash = HashHelpers.Combine(hash, GetHashCode(obj.M31)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M32)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M33)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M34)); + + hash = HashHelpers.Combine(hash, GetHashCode(obj.M41)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M42)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M43)); + hash = HashHelpers.Combine(hash, GetHashCode(obj.M44)); + return hash; + } + + + public static bool Equal(PointF a, PointF b, float tolerance) + { + return + Equal(a.X, b.X, tolerance) && + Equal(a.Y, b.Y, tolerance); + } + + public static bool Equal(PointF a, PointF b) + { + return Equal(a, b, defaultTolerance); + } + + public bool Equals(PointF a, PointF b) + { + return Equal(a, b, this.tolerance); + } + + + public int GetHashCode(PointF obj) + { + int hash = GetHashCode(obj.X); + hash = HashHelpers.Combine(hash, GetHashCode(obj.Y)); + return hash; + } + } +} diff --git a/src/SixLabors.Primitives/Ellipse.cs b/src/SixLabors.Primitives/Ellipse.cs index d5611bb23..e1890df27 100644 --- a/src/SixLabors.Primitives/Ellipse.cs +++ b/src/SixLabors.Primitives/Ellipse.cs @@ -171,13 +171,10 @@ namespace SixLabors.Primitives /// private int GetHashCode(Ellipse ellipse) { - unchecked - { - int hashCode = ellipse.center.GetHashCode(); - hashCode = (hashCode * 397) ^ ellipse.RadiusX.GetHashCode(); - hashCode = (hashCode * 397) ^ ellipse.RadiusY.GetHashCode(); - return hashCode; - } + int hashCode = ellipse.center.GetHashCode(); + hashCode = HashHelpers.Combine(hashCode, ellipse.RadiusX.GetHashCode()); + hashCode = HashHelpers.Combine(hashCode, ellipse.RadiusY.GetHashCode()); + return hashCode; } } } diff --git a/src/SixLabors.Primitives/HashHelpers.cs b/src/SixLabors.Primitives/HashHelpers.cs new file mode 100644 index 000000000..d622d308c --- /dev/null +++ b/src/SixLabors.Primitives/HashHelpers.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SixLabors.Primitives +{ + // lifted from coreFX repo + internal static class HashHelpers + { + public static readonly int RandomSeed = Guid.NewGuid().GetHashCode(); + + public static int Combine(int h1, int h2) + { + unchecked + { + // RyuJIT optimizes this to use the ROL instruction + // Related GitHub pull request: dotnet/coreclr#1830 + uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27); + return ((int)rol5 + h1) ^ h2; + } + } + } +} diff --git a/src/SixLabors.Primitives/LongRational.cs b/src/SixLabors.Primitives/LongRational.cs index d33484338..4f01936ee 100644 --- a/src/SixLabors.Primitives/LongRational.cs +++ b/src/SixLabors.Primitives/LongRational.cs @@ -347,9 +347,6 @@ namespace SixLabors.Primitives /// /// A 32-bit signed integer that is the hash code for this instance. /// - private int GetHashCode(LongRational rational) - { - return ((rational.Numerator * 397) ^ rational.Denominator).GetHashCode(); - } + private int GetHashCode(LongRational rational) => HashHelpers.Combine(rational.Numerator.GetHashCode(), rational.Denominator.GetHashCode()); } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/MathF.cs b/src/SixLabors.Primitives/MathF.cs index a15b7fb20..eeaca821b 100644 --- a/src/SixLabors.Primitives/MathF.cs +++ b/src/SixLabors.Primitives/MathF.cs @@ -94,7 +94,7 @@ namespace SixLabors.Primitives /// The representing the degree as radians. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DegreeToRadian(float degree) + public static float ToRadians(float degree) { return degree * (PI / 180F); } @@ -181,7 +181,7 @@ namespace SixLabors.Primitives /// The representing the degree as radians. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float RadianToDegree(float radian) + public static float ToDegree(float radian) { return radian / (PI / 180F); } diff --git a/src/SixLabors.Primitives/Matrix.cs b/src/SixLabors.Primitives/Matrix.cs index 6da436cfb..aa97c3ed7 100644 --- a/src/SixLabors.Primitives/Matrix.cs +++ b/src/SixLabors.Primitives/Matrix.cs @@ -32,9 +32,43 @@ namespace SixLabors.Primitives public bool IsIdentity => this.backingMatrix.IsIdentity; /// - /// Gets or sets the translation component of this matrix. + /// Gets or Sets the translation component of this matrix. /// - public Vector2 Translation => this.backingMatrix.Translation; + public PointF Translation + { + get => this.backingMatrix.Translation; + set => this.backingMatrix.Translation = value; + } + + /// + /// The first element of the first row + /// + public float M11 { get => this.backingMatrix.M11; set => this.backingMatrix.M11 = value; } + + /// + /// The second element of the first row + /// + public float M12 { get => this.backingMatrix.M12; set => this.backingMatrix.M12 = value; } + + /// + /// The first element of the second row + /// + public float M21 { get => this.backingMatrix.M21; set => this.backingMatrix.M21 = value; } + + /// + /// The second element of the second row + /// + public float M22 { get => this.backingMatrix.M22; set => this.backingMatrix.M22 = value; } + + /// + /// The first element of the third row + /// + public float M31 { get => this.backingMatrix.M31; set => this.backingMatrix.M31 = value; } + + /// + /// The second element of the third row + /// + public float M32 { get => this.backingMatrix.M32; set => this.backingMatrix.M32 = value; } /// /// Constructs a Matrix3x2 from the given components. @@ -122,7 +156,7 @@ namespace SixLabors.Primitives /// The X angle, in degrees. /// The Y angle, in degrees. /// A skew matrix. - public static Matrix CreateSkewDegrees(float degreesX, float degreesY) => Matrix3x2.CreateSkew(MathF.DegreeToRadian(degreesX), MathF.DegreeToRadian(degreesY)); + public static Matrix CreateSkewDegrees(float degreesX, float degreesY) => Matrix3x2.CreateSkew(MathF.ToRadians(degreesX), MathF.ToRadians(degreesY)); /// /// Creates a skew matrix from the given angles in radians and a center point. @@ -140,7 +174,7 @@ namespace SixLabors.Primitives /// The Y angle, in degrees. /// The center point. /// A skew matrix. - public static Matrix CreateSkewDegrees(float degreesX, float degreesY, PointF centerPoint) => Matrix3x2.CreateSkew(MathF.DegreeToRadian(degreesX), MathF.DegreeToRadian(degreesY), centerPoint); + public static Matrix CreateSkewDegrees(float degreesX, float degreesY, PointF centerPoint) => Matrix3x2.CreateSkew(MathF.ToRadians(degreesX), MathF.ToRadians(degreesY), centerPoint); /// /// Creates a rotation matrix using the given rotation in radians. @@ -154,7 +188,7 @@ namespace SixLabors.Primitives /// /// The amount of rotation, in degrees. /// A rotation matrix. - public static Matrix CreateRotationDegrees(float degrees) => System.Numerics.Matrix3x2.CreateRotation(MathF.DegreeToRadian(degrees)); + public static Matrix CreateRotationDegrees(float degrees) => System.Numerics.Matrix3x2.CreateRotation(MathF.ToRadians(degrees)); /// /// Creates a rotation matrix using the given rotation in radians and a center point. @@ -170,7 +204,7 @@ namespace SixLabors.Primitives /// The amount of rotation, in degrees. /// The center point. /// A rotation matrix. - public static Matrix CreateRotationDegrees(float degrees, PointF centerPoint) => Matrix3x2.CreateRotation(MathF.DegreeToRadian(degrees), centerPoint); + public static Matrix CreateRotationDegrees(float degrees, PointF centerPoint) => Matrix3x2.CreateRotation(MathF.ToRadians(degrees), centerPoint); /// /// Calculates the determinant for this matrix. @@ -356,5 +390,10 @@ namespace SixLabors.Primitives /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Matrix(Matrix3x2 matrix) => new Matrix { backingMatrix = matrix }; + + internal static Matrix CreateScale(Vector2 scale, object zero) + { + throw new NotImplementedException(); + } } } diff --git a/src/SixLabors.Primitives/Point.cs b/src/SixLabors.Primitives/Point.cs index 243cb8eb3..206d0545a 100644 --- a/src/SixLabors.Primitives/Point.cs +++ b/src/SixLabors.Primitives/Point.cs @@ -25,6 +25,11 @@ namespace SixLabors.Primitives /// public static readonly Point Empty = default(Point); + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly Point Zero = new Point(0, 0); + /// /// Initializes a new instance of the struct. /// @@ -95,6 +100,13 @@ namespace SixLabors.Primitives [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 . /// @@ -252,6 +264,17 @@ namespace SixLabors.Primitives private static short LowInt16(int n) => unchecked((short)(n & 0xffff)); - private int GetHashCode(Point point) => point.X ^ point.Y; + private int GetHashCode(Point point) => HashHelpers.Combine(point.X.GetHashCode(), point.Y.GetHashCode()); + + /// + /// Transforms a point by the given matrix. + /// + /// The source point + /// The transformation matrix. + /// + public static PointF Transform(Point position, Matrix matrix) + { + return Vector2.Transform(position, matrix); + } } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/PointF.cs b/src/SixLabors.Primitives/PointF.cs index a49a1996b..8df9ef7bf 100644 --- a/src/SixLabors.Primitives/PointF.cs +++ b/src/SixLabors.Primitives/PointF.cs @@ -25,6 +25,11 @@ namespace SixLabors.Primitives /// public static readonly PointF Empty = default(PointF); + /// + /// Represents a that has X and Y values set to zero. + /// + public static readonly PointF Zero = new PointF(0, 0); + /// /// Initializes a new instance of the struct. /// @@ -93,6 +98,13 @@ namespace SixLabors.Primitives [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 . /// @@ -104,6 +116,26 @@ namespace SixLabors.Primitives [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 . /// @@ -144,7 +176,7 @@ namespace SixLabors.Primitives public static bool operator !=(PointF left, PointF right) => !left.Equals(right); /// - /// Translates a by the negative of a given . + /// Translates a by the given . /// /// The point on the left hand of the operand. /// The size on the right hand of the operand. @@ -152,6 +184,15 @@ namespace SixLabors.Primitives [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 . /// @@ -161,6 +202,15 @@ namespace SixLabors.Primitives [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); + /// /// Rotates a point around the given rotation matrix. /// @@ -228,6 +278,17 @@ namespace SixLabors.Primitives /// /// A 32-bit signed integer that is the hash code for this instance. /// - private int GetHashCode(PointF point) => point.X.GetHashCode() ^ point.Y.GetHashCode(); + private int GetHashCode(PointF point) => HashHelpers.Combine(point.X.GetHashCode(), point.Y.GetHashCode()); + + /// + /// Transforms a point by the given matrix. + /// + /// The source point + /// The transformation matrix. + /// + public static PointF Transform(PointF position, Matrix matrix) + { + return Vector2.Transform(position, matrix); + } } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/Rectangle.cs b/src/SixLabors.Primitives/Rectangle.cs index 9c9a0599e..173ee5d1b 100644 --- a/src/SixLabors.Primitives/Rectangle.cs +++ b/src/SixLabors.Primitives/Rectangle.cs @@ -454,14 +454,24 @@ namespace SixLabors.Primitives private int GetHashCode(Rectangle rectangle) { - unchecked - { - int hashCode = rectangle.X; - hashCode = (hashCode * 397) ^ rectangle.Y; - hashCode = (hashCode * 397) ^ rectangle.Width; - hashCode = (hashCode * 397) ^ rectangle.Height; - return hashCode; - } + int hashCode = rectangle.X.GetHashCode(); + hashCode = HashHelpers.Combine(hashCode, rectangle.Y.GetHashCode()); + hashCode = HashHelpers.Combine(hashCode, rectangle.Width.GetHashCode()); + hashCode = HashHelpers.Combine(hashCode, rectangle.Height.GetHashCode()); + return hashCode; + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle + /// The transformation matrix. + /// + public static RectangleF Transform(Rectangle rectangle, Matrix 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)); } } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/RectangleF.cs b/src/SixLabors.Primitives/RectangleF.cs index c7d8b0ebb..d74fb796f 100644 --- a/src/SixLabors.Primitives/RectangleF.cs +++ b/src/SixLabors.Primitives/RectangleF.cs @@ -387,14 +387,24 @@ namespace SixLabors.Primitives private int GetHashCode(RectangleF rectangle) { - unchecked - { - int hashCode = rectangle.X.GetHashCode(); - hashCode = (hashCode * 397) ^ rectangle.Y.GetHashCode(); - hashCode = (hashCode * 397) ^ rectangle.Width.GetHashCode(); - hashCode = (hashCode * 397) ^ rectangle.Height.GetHashCode(); - return hashCode; - } + int hashCode = rectangle.X.GetHashCode(); + hashCode = HashHelpers.Combine(hashCode, rectangle.Y.GetHashCode()); + hashCode = HashHelpers.Combine(hashCode, rectangle.Width.GetHashCode()); + hashCode = HashHelpers.Combine(hashCode, rectangle.Height.GetHashCode()); + return hashCode; + } + + /// + /// Transforms a rectangle by the given matrix. + /// + /// The source rectangle + /// The transformation matrix. + /// + public static RectangleF Transform(RectangleF rectangle, Matrix 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)); } } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/Size.cs b/src/SixLabors.Primitives/Size.cs index 1605232b1..fb306f128 100644 --- a/src/SixLabors.Primitives/Size.cs +++ b/src/SixLabors.Primitives/Size.cs @@ -7,6 +7,7 @@ namespace SixLabors.Primitives { using System; using System.ComponentModel; + using System.Numerics; using System.Runtime.CompilerServices; /// @@ -22,6 +23,11 @@ namespace SixLabors.Primitives /// Represents a that has Width and Height values set to zero. /// public static readonly Size Empty = default(Size); + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly Size Zero = new Size(0, 0); + /// /// Initializes a new instance of the struct. @@ -220,6 +226,19 @@ namespace SixLabors.Primitives /// /// A 32-bit signed integer that is the hash code for this instance. /// - private int GetHashCode(Size size) => size.Width ^ size.Height; + private int GetHashCode(Size size) => HashHelpers.Combine(size.Width.GetHashCode(), size.Height.GetHashCode()); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size + /// The transformation matrix. + /// + public static SizeF Transform(Size size, Matrix matrix) + { + var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } } } \ No newline at end of file diff --git a/src/SixLabors.Primitives/SizeF.cs b/src/SixLabors.Primitives/SizeF.cs index a11d2e443..b822740cd 100644 --- a/src/SixLabors.Primitives/SizeF.cs +++ b/src/SixLabors.Primitives/SizeF.cs @@ -24,6 +24,11 @@ namespace SixLabors.Primitives /// public static readonly SizeF Empty = default(SizeF); + /// + /// Represents a that has Width and Height values set to zero. + /// + public static readonly SizeF Zero = new SizeF(0, 0); + /// /// Initializes a new instance of the struct. /// @@ -175,7 +180,7 @@ namespace SixLabors.Primitives [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); - private int GetHashCode(SizeF size) => size.Width.GetHashCode() ^ size.Height.GetHashCode(); + private int GetHashCode(SizeF size) => HashHelpers.Combine(size.Width.GetHashCode(), size.Height.GetHashCode()); /// /// Creates a with the coordinates of the specified . @@ -186,5 +191,18 @@ namespace SixLabors.Primitives /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Vector2(SizeF point) => new Vector2(point.Width, point.Height); + + /// + /// Transforms a size by the given matrix. + /// + /// The source size + /// The transformation matrix. + /// + public static SizeF Transform(SizeF size, Matrix matrix) + { + var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix); + + return new SizeF(v.X, v.Y); + } } } \ No newline at end of file diff --git a/tests/CodeCoverage/.gitignore b/tests/CodeCoverage/.gitignore new file mode 100644 index 000000000..861c19344 --- /dev/null +++ b/tests/CodeCoverage/.gitignore @@ -0,0 +1 @@ +/OpenCover.4.6.519 diff --git a/tests/CodeCoverage/CodeCoverage.cmd b/tests/CodeCoverage/CodeCoverage.cmd index 498e55b36..2547eace1 100644 --- a/tests/CodeCoverage/CodeCoverage.cmd +++ b/tests/CodeCoverage/CodeCoverage.cmd @@ -12,7 +12,7 @@ dotnet build SixLabors.Primitives.sln --no-incremental -c debug /p:codecov=true rem The -threshold options prevents this taking ages... rem tests\CodeCoverage\OpenCover.4.6.519\tools\OpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test tests\SixLabors.Shapes.Tests\SixLabors.Shapes.Tests.csproj --no-build -c Release /p:codecov=true" -threshold:10 -register:user -filter:"+[SixLabors.Shapes*]*" -excludebyattribute:*.ExcludeFromCodeCoverage* -hideskipped:All -returntargetcode -output:.\SixLabors.Shapes.Coverage.xml -tests\CodeCoverage\OpenCover.4.6.519\tools\OpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test tests\SixLabors.Primitives.Tests\SixLabors.Primitives.Tests.csproj --no-build -c debug" -searchdirs:"tests\SixLabors.Shapes.Tests\bin\Release\netcoreapp1.1" -register:user -output:.\SixLabors.Shapes.Coverage.xml -hideskipped:All -returntargetcode -oldStyle -filter:"+[SixLabors.Primitives*]*" +tests\CodeCoverage\OpenCover.4.6.519\tools\OpenCover.Console.exe -target:"dotnet.exe" -targetargs:"test tests\SixLabors.Primitives.Tests\SixLabors.Primitives.Tests.csproj --no-build -c debug" -searchdirs:"tests\SixLabors.Primitives.Tests\bin\Release\netcoreapp1.1" -register:user -output:.\SixLabors.Primitives.Coverage.xml -hideskipped:All -returntargetcode -oldStyle -filter:"+[SixLabors.Primitives*]*" if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/tests/SixLabors.Primitives.Tests/MatrixTests.cs b/tests/SixLabors.Primitives.Tests/MatrixTests.cs new file mode 100644 index 000000000..c228f7493 --- /dev/null +++ b/tests/SixLabors.Primitives.Tests/MatrixTests.cs @@ -0,0 +1,1015 @@ +// +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace SixLabors.Primitives.Tests +{ + using System.Globalization; + using System.Numerics; + using Xunit; + + /// + /// Tests the struct. + /// + public class MatrixTests + { + + [Fact] + public void ImplicitCastMatrixToMatrix3x2() + { + Matrix matrix = new Matrix(1, 2, 3, 4, 5, 6); + + Matrix3x2 convertedMatrix = matrix; + Assert.Equal(1, convertedMatrix.M11); + Assert.Equal(2, convertedMatrix.M12); + Assert.Equal(3, convertedMatrix.M21); + Assert.Equal(4, convertedMatrix.M22); + Assert.Equal(5, convertedMatrix.M31); + Assert.Equal(6, convertedMatrix.M32); + } + + [Fact] + public void ImplicitCastMatrix3x2ToMatrix() + { + Matrix3x2 matrix = new Matrix3x2(1, 2, 3, 4, 5, 6); + + Matrix convertedMatrix = matrix; + Assert.Equal(1, convertedMatrix.M11); + Assert.Equal(2, convertedMatrix.M12); + Assert.Equal(3, convertedMatrix.M21); + Assert.Equal(4, convertedMatrix.M22); + Assert.Equal(5, convertedMatrix.M31); + Assert.Equal(6, convertedMatrix.M32); + } + + /// matrix test mostly tken directly from CoreFX + /// + static Matrix GenerateMatrixNumberFrom1To6() + { + Matrix a = new Matrix(); + a.M11 = 1.0f; + a.M12 = 2.0f; + a.M21 = 3.0f; + a.M22 = 4.0f; + a.M31 = 5.0f; + a.M32 = 6.0f; + return a; + } + + static Matrix GenerateTestMatrix() + { + Matrix m = Matrix.CreateRotation(MathF.ToRadians(30.0f)); + m.Translation = new Vector2(111.0f, 222.0f); + return m; + } + + // A test for Identity + [Fact] + public void MatrixIdentityTest() + { + Matrix val = new Matrix(); + val.M11 = val.M22 = 1.0f; + + Assert.True(ApproximateFloatComparer.Equal(val, Matrix.Identity), "Matrix.Indentity was not set correctly."); + } + + // A test for Determinant + [Fact] + public void MatrixDeterminantTest() + { + Matrix target = Matrix.CreateRotation(MathF.ToRadians(30.0f)); + + float val = 1.0f; + float det = target.GetDeterminant(); + + Assert.True(ApproximateFloatComparer.Equal(val, det), "Matrix.Determinant was not set correctly."); + } + + // A test for Determinant + // Determinant test |A| = 1 / |A'| + [Fact] + public void MatrixDeterminantTest1() + { + Matrix a = new Matrix(); + a.M11 = 5.0f; + a.M12 = 2.0f; + a.M21 = 12.0f; + a.M22 = 6.8f; + a.M31 = 6.5f; + a.M32 = 1.0f; + Matrix i; + Assert.True(Matrix.Invert(a, out i)); + + float detA = a.GetDeterminant(); + float detI = i.GetDeterminant(); + float t = 1.0f / detI; + + // only accurate to 3 precision + Assert.True(System.Math.Abs(detA - t) < 1e-3, "Matrix.Determinant was not set correctly."); + + // sanity check against 4x4 version + Assert.Equal(new Matrix4x4(a).GetDeterminant(), detA); + Assert.Equal(new Matrix4x4(i).GetDeterminant(), detI); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertTest() + { + Matrix mtx = Matrix.CreateRotation(MathF.ToRadians(30.0f)); + + Matrix expected = new Matrix(); + expected.M11 = 0.8660254f; + expected.M12 = -0.5f; + + expected.M21 = 0.5f; + expected.M22 = 0.8660254f; + + expected.M31 = 0; + expected.M32 = 0; + + Matrix actual; + + Assert.True(Matrix.Invert(mtx, out actual)); + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.Invert did not return the expected value."); + + Matrix i = mtx * actual; + Assert.True(ApproximateFloatComparer.Equal(i, Matrix.Identity), "Matrix.Invert did not return the expected value."); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertIdentityTest() + { + Matrix mtx = Matrix.Identity; + + Matrix actual; + Assert.True(Matrix.Invert(mtx, out actual)); + + Assert.True(ApproximateFloatComparer.Equal(actual, Matrix.Identity)); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertTranslationTest() + { + Matrix mtx = Matrix.CreateTranslation(23, 42); + + Matrix actual; + Assert.True(Matrix.Invert(mtx, out actual)); + + Matrix i = mtx * actual; + Assert.True(ApproximateFloatComparer.Equal(i, Matrix.Identity)); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertRotationTest() + { + Matrix mtx = Matrix.CreateRotation(2); + + Matrix actual; + Assert.True(Matrix.Invert(mtx, out actual)); + + Matrix i = mtx * actual; + Assert.True(ApproximateFloatComparer.Equal(i, Matrix.Identity)); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertScaleTest() + { + Matrix mtx = Matrix.CreateScale(23, -42); + + Matrix actual; + Assert.True(Matrix.Invert(mtx, out actual)); + + Matrix i = mtx * actual; + Assert.True(ApproximateFloatComparer.Equal(i, Matrix.Identity)); + } + + // A test for Invert (Matrix) + [Fact] + public void MatrixInvertAffineTest() + { + Matrix mtx = Matrix.CreateRotation(2) * + Matrix.CreateScale(23, -42) * + Matrix.CreateTranslation(17, 53); + + Matrix actual; + Assert.True(Matrix.Invert(mtx, out actual)); + + Matrix i = mtx * actual; + Assert.True(ApproximateFloatComparer.Equal(i, Matrix.Identity)); + } + + // A test for CreateRotation (float) + [Fact] + public void MatrixCreateRotationTest() + { + float radians = MathF.ToRadians(50.0f); + + Matrix expected = new Matrix(); + expected.M11 = 0.642787635f; + expected.M12 = 0.766044438f; + expected.M21 = -0.766044438f; + expected.M22 = 0.642787635f; + + Matrix actual; + actual = Matrix.CreateRotation(radians); + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.CreateRotation did not return the expected value."); + } + + // A test for CreateRotation (float, Vector2f) + [Fact] + public void MatrixCreateRotationCenterTest() + { + float radians = MathF.ToRadians(30.0f); + Vector2 center = new Vector2(23, 42); + + Matrix rotateAroundZero = Matrix.CreateRotation(radians, Vector2.Zero); + Matrix rotateAroundZeroExpected = Matrix.CreateRotation(radians); + Assert.True(ApproximateFloatComparer.Equal(rotateAroundZero, rotateAroundZeroExpected)); + + Matrix rotateAroundCenter = Matrix.CreateRotation(radians, center); + Matrix rotateAroundCenterExpected = Matrix.CreateTranslation(-center) * Matrix.CreateRotation(radians) * Matrix.CreateTranslation(center); + Assert.True(ApproximateFloatComparer.Equal(rotateAroundCenter, rotateAroundCenterExpected)); + } + + // A test for CreateRotation (float) + [Fact] + public void MatrixCreateRotationRightAngleTest() + { + // 90 degree rotations must be exact! + Matrix actual = Matrix.CreateRotation(0); + Assert.Equal(new Matrix(1, 0, 0, 1, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI / 2); + Assert.Equal(new Matrix(0, 1, -1, 0, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI); + Assert.Equal(new Matrix(-1, 0, 0, -1, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI * 3 / 2); + Assert.Equal(new Matrix(0, -1, 1, 0, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI * 2); + Assert.Equal(new Matrix(1, 0, 0, 1, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI * 5 / 2); + Assert.Equal(new Matrix(0, 1, -1, 0, 0, 0), actual); + + actual = Matrix.CreateRotation(-MathF.PI / 2); + Assert.Equal(new Matrix(0, -1, 1, 0, 0, 0), actual); + + // But merely close-to-90 rotations should not be excessively clamped. + float delta = MathF.ToRadians(0.01f); + + actual = Matrix.CreateRotation(MathF.PI + delta); + Assert.False(ApproximateFloatComparer.Equal(new Matrix(-1, 0, 0, -1, 0, 0), actual)); + + actual = Matrix.CreateRotation(MathF.PI - delta); + Assert.False(ApproximateFloatComparer.Equal(new Matrix(-1, 0, 0, -1, 0, 0), actual)); + } + + // A test for CreateRotation (float, Vector2f) + [Fact] + public void MatrixCreateRotationRightAngleCenterTest() + { + Vector2 center = new Vector2(3, 7); + + // 90 degree rotations must be exact! + Matrix actual = Matrix.CreateRotation(0, center); + Assert.Equal(new Matrix(1, 0, 0, 1, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI / 2, center); + Assert.Equal(new Matrix(0, 1, -1, 0, 10, 4), actual); + + actual = Matrix.CreateRotation(MathF.PI, center); + Assert.Equal(new Matrix(-1, 0, 0, -1, 6, 14), actual); + + actual = Matrix.CreateRotation(MathF.PI * 3 / 2, center); + Assert.Equal(new Matrix(0, -1, 1, 0, -4, 10), actual); + + actual = Matrix.CreateRotation(MathF.PI * 2, center); + Assert.Equal(new Matrix(1, 0, 0, 1, 0, 0), actual); + + actual = Matrix.CreateRotation(MathF.PI * 5 / 2, center); + Assert.Equal(new Matrix(0, 1, -1, 0, 10, 4), actual); + + actual = Matrix.CreateRotation(-MathF.PI / 2, center); + Assert.Equal(new Matrix(0, -1, 1, 0, -4, 10), actual); + + // But merely close-to-90 rotations should not be excessively clamped. + float delta = MathF.ToRadians(0.01f); + + actual = Matrix.CreateRotation(MathF.PI + delta, center); + Assert.False(ApproximateFloatComparer.Equal(new Matrix(-1, 0, 0, -1, 6, 14), actual)); + + actual = Matrix.CreateRotation(MathF.PI - delta, center); + Assert.False(ApproximateFloatComparer.Equal(new Matrix(-1, 0, 0, -1, 6, 14), actual)); + } + + // A test for Invert (Matrix) + // Non invertible matrix - determinant is zero - singular matrix + [Fact] + public void MatrixInvertTest1() + { + Matrix a = new Matrix(); + a.M11 = 0.0f; + a.M12 = 2.0f; + a.M21 = 0.0f; + a.M22 = 4.0f; + a.M31 = 5.0f; + a.M32 = 6.0f; + + float detA = a.GetDeterminant(); + Assert.True(ApproximateFloatComparer.Equal(detA, 0.0f), "Matrix.Invert did not return the expected value."); + + Matrix actual; + Assert.False(Matrix.Invert(a, out actual)); + + // all the elements in Actual is NaN + Assert.True( + float.IsNaN(actual.M11) && float.IsNaN(actual.M12) && + float.IsNaN(actual.M21) && float.IsNaN(actual.M22) && + float.IsNaN(actual.M31) && float.IsNaN(actual.M32) + , "Matrix.Invert did not return the expected value."); + } + + // A test for Lerp (Matrix, Matrix, float) + [Fact] + public void MatrixLerpTest() + { + Matrix a = new Matrix(); + a.M11 = 11.0f; + a.M12 = 12.0f; + a.M21 = 21.0f; + a.M22 = 22.0f; + a.M31 = 31.0f; + a.M32 = 32.0f; + + Matrix b = GenerateMatrixNumberFrom1To6(); + + float t = 0.5f; + + Matrix expected = new Matrix(); + expected.M11 = a.M11 + (b.M11 - a.M11) * t; + expected.M12 = a.M12 + (b.M12 - a.M12) * t; + + expected.M21 = a.M21 + (b.M21 - a.M21) * t; + expected.M22 = a.M22 + (b.M22 - a.M22) * t; + + expected.M31 = a.M31 + (b.M31 - a.M31) * t; + expected.M32 = a.M32 + (b.M32 - a.M32) * t; + + Matrix actual; + actual = Matrix.Lerp(a, b, t); + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.Lerp did not return the expected value."); + } + + // A test for operator - (Matrix) + [Fact] + public void MatrixUnaryNegationTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = -1.0f; + expected.M12 = -2.0f; + expected.M21 = -3.0f; + expected.M22 = -4.0f; + expected.M31 = -5.0f; + expected.M32 = -6.0f; + + Matrix actual = -a; + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.operator - did not return the expected value."); + } + + // A test for operator - (Matrix, Matrix) + [Fact] + public void MatrixSubtractionTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + Matrix expected = new Matrix(); + + Matrix actual = a - b; + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.operator - did not return the expected value."); + } + + // A test for operator * (Matrix, Matrix) + [Fact] + public void MatrixMultiplyTest1() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = a.M11 * b.M11 + a.M12 * b.M21; + expected.M12 = a.M11 * b.M12 + a.M12 * b.M22; + + expected.M21 = a.M21 * b.M11 + a.M22 * b.M21; + expected.M22 = a.M21 * b.M12 + a.M22 * b.M22; + + expected.M31 = a.M31 * b.M11 + a.M32 * b.M21 + b.M31; + expected.M32 = a.M31 * b.M12 + a.M32 * b.M22 + b.M32; + + Matrix actual = a * b; + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.operator * did not return the expected value."); + + // Sanity check by comparison with 4x4 multiply. + a = Matrix.CreateRotation(MathF.ToRadians(30)) * Matrix.CreateTranslation(23, 42); + b = Matrix.CreateScale(3, 7) * Matrix.CreateTranslation(666, -1); + + actual = a * b; + + Matrix4x4 a44 = new Matrix4x4(a); + Matrix4x4 b44 = new Matrix4x4(b); + Matrix4x4 expected44 = a44 * b44; + Matrix4x4 actual44 = new Matrix4x4(actual); + + Assert.True(ApproximateFloatComparer.Equal(expected44, actual44), "Matrix.operator * did not return the expected value."); + } + + // A test for operator * (Matrix, Matrix) + // Multiply with identity matrix + [Fact] + public void MatrixMultiplyTest4() + { + Matrix a = new Matrix(); + a.M11 = 1.0f; + a.M12 = 2.0f; + a.M21 = 5.0f; + a.M22 = -6.0f; + a.M31 = 9.0f; + a.M32 = 10.0f; + + Matrix b = new Matrix(); + b = Matrix.Identity; + + Matrix expected = a; + Matrix actual = a * b; + + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.operator * did not return the expected value."); + } + + // A test for operator + (Matrix, Matrix) + [Fact] + public void MatrixAdditionTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = a.M11 + b.M11; + expected.M12 = a.M12 + b.M12; + expected.M21 = a.M21 + b.M21; + expected.M22 = a.M22 + b.M22; + expected.M31 = a.M31 + b.M31; + expected.M32 = a.M32 + b.M32; + + Matrix actual; + + actual = a + b; + + Assert.True(ApproximateFloatComparer.Equal(expected, actual), "Matrix.operator + did not return the expected value."); + } + + // A test for ToString () + [Fact] + public void MatrixToStringTest() + { + Matrix a = new Matrix(); + a.M11 = 11.0f; + a.M12 = -12.0f; + a.M21 = 21.0f; + a.M22 = 22.0f; + a.M31 = 31.0f; + a.M32 = 32.0f; + + string expected = "{ {M11:11 M12:-12} " + + "{M21:21 M22:22} " + + "{M31:31 M32:32} }"; + string actual; + + actual = a.ToString(); + Assert.Equal(expected, actual); + } + + // A test for Add (Matrix, Matrix) + [Fact] + public void MatrixAddTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = a.M11 + b.M11; + expected.M12 = a.M12 + b.M12; + expected.M21 = a.M21 + b.M21; + expected.M22 = a.M22 + b.M22; + expected.M31 = a.M31 + b.M31; + expected.M32 = a.M32 + b.M32; + + Matrix actual; + + actual = Matrix.Add(a, b); + Assert.Equal(expected, actual); + } + + // A test for Equals (object) + [Fact] + public void MatrixEqualsTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + // case 1: compare between same values + object obj = b; + + bool expected = true; + bool actual = a.Equals(obj); + Assert.Equal(expected, actual); + + // case 2: compare between different values + b.M11 = 11.0f; + obj = b; + expected = false; + actual = a.Equals(obj); + Assert.Equal(expected, actual); + + // case 3: compare between different types. + obj = new Vector4(); + expected = false; + actual = a.Equals(obj); + Assert.Equal(expected, actual); + + // case 3: compare against null. + obj = null; + expected = false; + actual = a.Equals(obj); + Assert.Equal(expected, actual); + } + + // A test for GetHashCode () + [Fact] + public void MatrixGetHashCodeTest() + { + Matrix target = GenerateMatrixNumberFrom1To6(); + int expected = unchecked(target.M11.GetHashCode() + target.M12.GetHashCode() + + target.M21.GetHashCode() + target.M22.GetHashCode() + + target.M31.GetHashCode() + target.M32.GetHashCode()); + int actual; + + actual = target.GetHashCode(); + Assert.Equal(expected, actual); + } + + // A test for Multiply (Matrix, Matrix) + [Fact] + public void MatrixMultiplyTest3() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = a.M11 * b.M11 + a.M12 * b.M21; + expected.M12 = a.M11 * b.M12 + a.M12 * b.M22; + + expected.M21 = a.M21 * b.M11 + a.M22 * b.M21; + expected.M22 = a.M21 * b.M12 + a.M22 * b.M22; + + expected.M31 = a.M31 * b.M11 + a.M32 * b.M21 + b.M31; + expected.M32 = a.M31 * b.M12 + a.M32 * b.M22 + b.M32; + Matrix actual; + actual = Matrix.Multiply(a, b); + + Assert.Equal(expected, actual); + + // Sanity check by comparison with 4x4 multiply. + a = Matrix.CreateRotation(MathF.ToRadians(30)) * Matrix.CreateTranslation(23, 42); + b = Matrix.CreateScale(3, 7) * Matrix.CreateTranslation(666, -1); + + actual = Matrix.Multiply(a, b); + + Matrix4x4 a44 = new Matrix4x4(a); + Matrix4x4 b44 = new Matrix4x4(b); + Matrix4x4 expected44 = Matrix4x4.Multiply(a44, b44); + Matrix4x4 actual44 = new Matrix4x4(actual); + + Assert.True(ApproximateFloatComparer.Equal(expected44, actual44), "Matrix.Multiply did not return the expected value."); + } + + // A test for Multiply (Matrix, float) + [Fact] + public void MatrixMultiplyTest5() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix expected = new Matrix(3, 6, 9, 12, 15, 18); + Matrix actual = Matrix.Multiply(a, 3); + + Assert.Equal(expected, actual); + } + + // A test for Multiply (Matrix, float) + [Fact] + public void MatrixMultiplyTest6() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix expected = new Matrix(3, 6, 9, 12, 15, 18); + Matrix actual = a * 3; + + Assert.Equal(expected, actual); + } + + // A test for Negate (Matrix) + [Fact] + public void MatrixNegateTest() + { + Matrix m = GenerateMatrixNumberFrom1To6(); + + Matrix expected = new Matrix(); + expected.M11 = -1.0f; + expected.M12 = -2.0f; + expected.M21 = -3.0f; + expected.M22 = -4.0f; + expected.M31 = -5.0f; + expected.M32 = -6.0f; + Matrix actual; + + actual = Matrix.Negate(m); + Assert.Equal(expected, actual); + } + + // A test for operator != (Matrix, Matrix) + [Fact] + public void MatrixInequalityTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + // case 1: compare between same values + bool expected = false; + bool actual = a != b; + Assert.Equal(expected, actual); + + // case 2: compare between different values + b.M11 = 11.0f; + expected = true; + actual = a != b; + Assert.Equal(expected, actual); + } + + // A test for operator == (Matrix, Matrix) + [Fact] + public void MatrixEqualityTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + // case 1: compare between same values + bool expected = true; + bool actual = a == b; + Assert.Equal(expected, actual); + + // case 2: compare between different values + b.M11 = 11.0f; + expected = false; + actual = a == b; + Assert.Equal(expected, actual); + } + + // A test for Subtract (Matrix, Matrix) + [Fact] + public void MatrixSubtractTest() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + Matrix expected = new Matrix(); + Matrix actual; + + actual = Matrix.Subtract(a, b); + Assert.Equal(expected, actual); + } + + // A test for CreateScale (Vector2f) + [Fact] + public void MatrixCreateScaleTest1() + { + SizeF scales = new SizeF(2.0f, 3.0f); + Matrix expected = new Matrix( + 2.0f, 0.0f, + 0.0f, 3.0f, + 0.0f, 0.0f); + Matrix actual = Matrix.CreateScale(scales); + Assert.Equal(expected, actual); + } + + // A test for CreateScale (Vector2f, Vector2f) + [Fact] + public void MatrixCreateScaleCenterTest1() + { + SizeF scale = new SizeF(3, 4); + PointF center = new PointF(23, 42); + + Matrix scaleAroundZero = Matrix.CreateScale(scale, PointF.Zero); + Matrix scaleAroundZeroExpected = Matrix.CreateScale(scale); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundZero, scaleAroundZeroExpected)); + + Matrix scaleAroundCenter = Matrix.CreateScale(scale, center); + Matrix scaleAroundCenterExpected = Matrix.CreateTranslation(-center) * Matrix.CreateScale(scale) * Matrix.CreateTranslation(center); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundCenter, scaleAroundCenterExpected)); + } + + // A test for CreateScale (float) + [Fact] + public void MatrixCreateScaleTest2() + { + float scale = 2.0f; + Matrix expected = new Matrix( + 2.0f, 0.0f, + 0.0f, 2.0f, + 0.0f, 0.0f); + Matrix actual = Matrix.CreateScale(scale); + Assert.Equal(expected, actual); + } + + // A test for CreateScale (float, Vector2f) + [Fact] + public void MatrixCreateScaleCenterTest2() + { + float scale = 5; + PointF center = new PointF(23, 42); + + Matrix scaleAroundZero = Matrix.CreateScale(scale, PointF.Zero); + Matrix scaleAroundZeroExpected = Matrix.CreateScale(scale); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundZero, scaleAroundZeroExpected)); + + Matrix scaleAroundCenter = Matrix.CreateScale(scale, center); + Matrix scaleAroundCenterExpected = Matrix.CreateTranslation(-center) * Matrix.CreateScale(scale) * Matrix.CreateTranslation(center); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundCenter, scaleAroundCenterExpected)); + } + + // A test for CreateScale (float, float) + [Fact] + public void MatrixCreateScaleTest3() + { + float xScale = 2.0f; + float yScale = 3.0f; + Matrix expected = new Matrix( + 2.0f, 0.0f, + 0.0f, 3.0f, + 0.0f, 0.0f); + Matrix actual = Matrix.CreateScale(xScale, yScale); + Assert.Equal(expected, actual); + } + + // A test for CreateScale (float, float, Vector2f) + [Fact] + public void MatrixCreateScaleCenterTest3() + { + SizeF scale = new SizeF(3, 4); + PointF center = new PointF(23, 42); + + Matrix scaleAroundZero = Matrix.CreateScale(scale.Width, scale.Height, Vector2.Zero); + Matrix scaleAroundZeroExpected = Matrix.CreateScale(scale.Width, scale.Height); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundZero, scaleAroundZeroExpected)); + + Matrix scaleAroundCenter = Matrix.CreateScale(scale.Width, scale.Height, center); + Matrix scaleAroundCenterExpected = Matrix.CreateTranslation(-center) * Matrix.CreateScale(scale.Width, scale.Height) * Matrix.CreateTranslation(center); + Assert.True(ApproximateFloatComparer.Equal(scaleAroundCenter, scaleAroundCenterExpected)); + } + + // A test for CreateTranslation (Vector2f) + [Fact] + public void MatrixCreateTranslationTest1() + { + PointF position = new PointF(2.0f, 3.0f); + Matrix expected = new Matrix( + 1.0f, 0.0f, + 0.0f, 1.0f, + 2.0f, 3.0f); + + Matrix actual = Matrix.CreateTranslation(position); + Assert.Equal(expected, actual); + } + + // A test for CreateTranslation (float, float) + [Fact] + public void MatrixCreateTranslationTest2() + { + float xPosition = 2.0f; + float yPosition = 3.0f; + + Matrix expected = new Matrix( + 1.0f, 0.0f, + 0.0f, 1.0f, + 2.0f, 3.0f); + + Matrix actual = Matrix.CreateTranslation(xPosition, yPosition); + Assert.Equal(expected, actual); + } + + // A test for Translation + [Fact] + public void MatrixTranslationTest() + { + Matrix a = GenerateTestMatrix(); + Matrix b = a; + + // Transformed vector that has same semantics of property must be same. + PointF val = new PointF(a.M31, a.M32); + Assert.Equal(val, a.Translation); + + // Set value and get value must be same. + val = new PointF(1.0f, 2.0f); + a.Translation = val; + Assert.Equal(val, a.Translation); + + // Make sure it only modifies expected value of matrix. + Assert.True( + a.M11 == b.M11 && a.M12 == b.M12 && + a.M21 == b.M21 && a.M22 == b.M22 && + a.M31 != b.M31 && a.M32 != b.M32, + "Matrix.Translation modified unexpected value of matrix."); + } + + // A test for Equals (Matrix) + [Fact] + public void MatrixEqualsTest1() + { + Matrix a = GenerateMatrixNumberFrom1To6(); + Matrix b = GenerateMatrixNumberFrom1To6(); + + // case 1: compare between same values + bool expected = true; + bool actual = a.Equals(b); + Assert.Equal(expected, actual); + + // case 2: compare between different values + b.M11 = 11.0f; + expected = false; + actual = a.Equals(b); + Assert.Equal(expected, actual); + } + + // A test for CreateSkew (float, float) + [Fact] + public void MatrixCreateSkewIdentityTest() + { + Matrix expected = Matrix.Identity; + Matrix actual = Matrix.CreateSkew(0, 0); + Assert.Equal(expected, actual); + } + + // A test for CreateSkew (float, float) + [Fact] + public void MatrixCreateSkewXTest() + { + Matrix expected = new Matrix(1, 0, -0.414213562373095f, 1, 0, 0); + Matrix actual = Matrix.CreateSkew(-MathF.PI / 8, 0); + Assert.True(ApproximateFloatComparer.Equal(expected, actual)); + + expected = new Matrix(1, 0, 0.414213562373095f, 1, 0, 0); + actual = Matrix.CreateSkew(MathF.PI / 8, 0); + Assert.True(ApproximateFloatComparer.Equal(expected, actual)); + + PointF result = PointF.Transform(new PointF(0, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new PointF(0, 0), result)); + + result = PointF.Transform(new Vector2(0, 1), actual); + Assert.True(ApproximateFloatComparer.Equal(new PointF(0.414213568f, 1), result)); + result = PointF.Transform(new PointF(0, -1), actual); + Assert.True(ApproximateFloatComparer.Equal(new PointF(-0.414213568f, -1), result)); + + result = PointF.Transform(new PointF(3, 10), actual); + Assert.True(ApproximateFloatComparer.Equal(new PointF(7.14213568f, 10), result)); + } + + // A test for CreateSkew (float, float) + [Fact] + public void MatrixCreateSkewYTest() + { + Matrix expected = new Matrix(1, -0.414213562373095f, 0, 1, 0, 0); + Matrix actual = Matrix.CreateSkew(0, -MathF.PI / 8); + Assert.True(ApproximateFloatComparer.Equal(expected, actual)); + + expected = new Matrix(1, 0.414213562373095f, 0, 1, 0, 0); + actual = Matrix.CreateSkew(0, MathF.PI / 8); + Assert.True(ApproximateFloatComparer.Equal(expected, actual)); + + Vector2 result = Vector2.Transform(new Vector2(0, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(0, 0), result)); + + result = Vector2.Transform(new Vector2(1, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(1, 0.414213568f), result)); + + result = Vector2.Transform(new Vector2(-1, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(-1, -0.414213568f), result)); + + result = Vector2.Transform(new Vector2(10, 3), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(10, 7.14213568f), result)); + } + + // A test for CreateSkew (float, float) + [Fact] + public void MatrixCreateSkewXYTest() + { + Matrix expected = new Matrix(1, -0.414213562373095f, 1, 1, 0, 0); + Matrix actual = Matrix.CreateSkew(MathF.PI / 4, -MathF.PI / 8); + Assert.True(ApproximateFloatComparer.Equal(expected, actual)); + + Vector2 result = Vector2.Transform(new Vector2(0, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(0, 0), result)); + + result = Vector2.Transform(new Vector2(1, 0), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(1, -0.414213562373095f), result)); + + result = Vector2.Transform(new Vector2(0, 1), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(1, 1), result)); + + result = Vector2.Transform(new Vector2(1, 1), actual); + Assert.True(ApproximateFloatComparer.Equal(new Vector2(2, 0.585786437626905f), result)); + } + + // A test for CreateSkew (float, float, Vector2f) + [Fact] + public void MatrixCreateSkewCenterTest() + { + float skewX = 1, skewY = 2; + Vector2 center = new Vector2(23, 42); + + Matrix skewAroundZero = Matrix.CreateSkew(skewX, skewY, Vector2.Zero); + Matrix skewAroundZeroExpected = Matrix.CreateSkew(skewX, skewY); + Assert.True(ApproximateFloatComparer.Equal(skewAroundZero, skewAroundZeroExpected)); + + Matrix skewAroundCenter = Matrix.CreateSkew(skewX, skewY, center); + Matrix skewAroundCenterExpected = Matrix.CreateTranslation(-center) * Matrix.CreateSkew(skewX, skewY) * Matrix.CreateTranslation(center); + Assert.True(ApproximateFloatComparer.Equal(skewAroundCenter, skewAroundCenterExpected)); + } + + // A test for IsIdentity + [Fact] + public void MatrixIsIdentityTest() + { + Assert.True(Matrix.Identity.IsIdentity); + Assert.True(new Matrix(1, 0, 0, 1, 0, 0).IsIdentity); + Assert.False(new Matrix(0, 0, 0, 1, 0, 0).IsIdentity); + Assert.False(new Matrix(1, 1, 0, 1, 0, 0).IsIdentity); + Assert.False(new Matrix(1, 0, 1, 1, 0, 0).IsIdentity); + Assert.False(new Matrix(1, 0, 0, 0, 0, 0).IsIdentity); + Assert.False(new Matrix(1, 0, 0, 1, 1, 0).IsIdentity); + Assert.False(new Matrix(1, 0, 0, 1, 0, 1).IsIdentity); + } + + // A test for Matrix comparison involving NaN values + [Fact] + public void MatrixEqualsNanTest() + { + Matrix a = new Matrix(float.NaN, 0, 0, 0, 0, 0); + Matrix b = new Matrix(0, float.NaN, 0, 0, 0, 0); + Matrix c = new Matrix(0, 0, float.NaN, 0, 0, 0); + Matrix d = new Matrix(0, 0, 0, float.NaN, 0, 0); + Matrix e = new Matrix(0, 0, 0, 0, float.NaN, 0); + Matrix f = new Matrix(0, 0, 0, 0, 0, float.NaN); + + Assert.False(a == new Matrix()); + Assert.False(b == new Matrix()); + Assert.False(c == new Matrix()); + Assert.False(d == new Matrix()); + Assert.False(e == new Matrix()); + Assert.False(f == new Matrix()); + + Assert.True(a != new Matrix()); + Assert.True(b != new Matrix()); + Assert.True(c != new Matrix()); + Assert.True(d != new Matrix()); + Assert.True(e != new Matrix()); + Assert.True(f != new Matrix()); + + Assert.False(a.Equals(new Matrix())); + Assert.False(b.Equals(new Matrix())); + Assert.False(c.Equals(new Matrix())); + Assert.False(d.Equals(new Matrix())); + Assert.False(e.Equals(new Matrix())); + Assert.False(f.Equals(new Matrix())); + + Assert.False(a.IsIdentity); + Assert.False(b.IsIdentity); + Assert.False(c.IsIdentity); + Assert.False(d.IsIdentity); + Assert.False(e.IsIdentity); + Assert.False(f.IsIdentity); + + // Counterintuitive result - IEEE rules for NaN comparison are weird! + Assert.False(a.Equals(a)); + Assert.False(b.Equals(b)); + Assert.False(c.Equals(c)); + Assert.False(d.Equals(d)); + Assert.False(e.Equals(e)); + Assert.False(f.Equals(f)); + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Primitives.Tests/PointFTests.cs b/tests/SixLabors.Primitives.Tests/PointFTests.cs index b6dd64511..28429f057 100644 --- a/tests/SixLabors.Primitives.Tests/PointFTests.cs +++ b/tests/SixLabors.Primitives.Tests/PointFTests.cs @@ -3,7 +3,7 @@ // Licensed under the Apache License, Version 2.0. // -namespace SixLabors.Primitives.Tests.Numerics +namespace SixLabors.Primitives.Tests { using System; using System.Globalization; diff --git a/tests/SixLabors.Primitives.Tests/SixLabors.Primitives.Tests.csproj b/tests/SixLabors.Primitives.Tests/SixLabors.Primitives.Tests.csproj index a300a0b06..2dd674fdc 100644 --- a/tests/SixLabors.Primitives.Tests/SixLabors.Primitives.Tests.csproj +++ b/tests/SixLabors.Primitives.Tests/SixLabors.Primitives.Tests.csproj @@ -3,7 +3,7 @@ 0.0.0 netcoreapp1.1 - SixLabors.Shapes.Tests + SixLabors.Primitives.Tests SixLabors.Shapes.Tests true false @@ -17,7 +17,6 @@ -