diff --git a/src/Spatial.Tests/Units/AngleTests.cs b/src/Spatial.Tests/Units/AngleTests.cs index 8e60df4..d51a2bc 100644 --- a/src/Spatial.Tests/Units/AngleTests.cs +++ b/src/Spatial.Tests/Units/AngleTests.cs @@ -109,11 +109,119 @@ public void EqualsWithTolerance() Assert.AreEqual(false, one.Equals(two, Angle.FromRadians(0.1))); } + [TestCase(0, 1)] + [TestCase(30, 0.86602540378443871)] + [TestCase(-30, 0.86602540378443871)] + [TestCase(45, 0.70710678118654757)] + [TestCase(-45, 0.70710678118654757)] + [TestCase(60, 0.5)] + [TestCase(-60, 0.5)] + [TestCase(90, 0)] + [TestCase(-90, 0)] + [TestCase(120, -0.5)] + [TestCase(-120, -0.5)] + [TestCase(180, -1)] + [TestCase(-180, -1)] + [TestCase(270, 0)] + public void CosineRoundTrip(double degrees, double cosine) + { + var angle = Angle.FromDegrees(degrees); + Assert.AreEqual(cosine, angle.Cos, 1e-15); + + if (degrees >= 0 && degrees <= 180) + { + var recovered = Angle.Acos(cosine); + Assert.AreEqual(angle.Degrees, recovered.Degrees, 1e-6); + } + } + + [Test] + public void AcosException() + { + Assert.Throws(() => Angle.Acos(5)); + } + + [TestCase(0, 0)] + [TestCase(30, 0.5)] + [TestCase(-30, -0.5)] + [TestCase(45, 0.70710678118654757)] + [TestCase(-45, -0.70710678118654757)] + [TestCase(60, 0.86602540378443871)] + [TestCase(-60, -0.86602540378443871)] + [TestCase(90, 1)] + [TestCase(-90, -1)] + [TestCase(120, 0.86602540378443871)] + [TestCase(-120, -0.86602540378443871)] + [TestCase(180, 0)] + [TestCase(-180, 0)] + [TestCase(270, -1)] + public void SineWithRoundTrip(double degrees, double sine) + { + var angle = Angle.FromDegrees(degrees); + Assert.AreEqual(sine, angle.Sin, 1e-15); + + if (degrees >= -90 && degrees <= 90) + { + var recovered = Angle.Asin(sine); + Assert.AreEqual(angle.Degrees, recovered.Degrees, 1e-6); + } + } + + [Test] + public void AsinException() + { + Assert.Throws(() => Angle.Asin(5)); + } + + [TestCase(0, 0)] + [TestCase(30, 0.57735026918962573)] + [TestCase(-30, -0.57735026918962573)] + [TestCase(45, 1)] + [TestCase(-45, -1)] + [TestCase(60, 1.7320508075688767)] + [TestCase(-60, -1.7320508075688767)] + [TestCase(120, -1.7320508075688783)] + [TestCase(-120, 1.7320508075688783)] + [TestCase(180, 0)] + [TestCase(-180, 0)] + public void TangentWithRoundTrip(double degrees, double tangent) + { + var angle = Angle.FromDegrees(degrees); + Assert.AreEqual(tangent, angle.Tan, 1e-15); + + if (degrees >= -90 && degrees <= 90) + { + var recovered = Angle.Atan(tangent); + Assert.AreEqual(angle.Degrees, recovered.Degrees, 1e-6); + } + } + + [TestCase(0, 1, 0)] + [TestCase(30, 1.7320508075688772935274463415059, 1)] + [TestCase(-30, 1.7320508075688772935274463415059, -1)] + [TestCase(45, 1, 1)] + [TestCase(-45, 1, -1)] + [TestCase(60, 1, 1.7320508075688772935274463415059)] + [TestCase(-60, 1, -1.7320508075688772935274463415059)] + [TestCase(90, 0, 1)] + [TestCase(-90, 0, -1)] + [TestCase(120, -1, 1.7320508075688772935274463415059)] + [TestCase(-120, -1, -1.7320508075688772935274463415059)] + [TestCase(150, -1.7320508075688772935274463415059, 1)] + [TestCase(-150, -1.7320508075688772935274463415059, -1)] + [TestCase(180, -1, 0)] + public void Atan2(double degrees, double x, double y) + { + var expected = Angle.FromDegrees(degrees); + var actual = Angle.Atan2(y, x); + Assert.AreEqual(expected.Degrees, actual.Degrees, 1e-10); + } + [TestCase(90, 1.5707963267948966)] public void FromDegrees(double degrees, double expected) { Assert.AreEqual(expected, Angle.FromDegrees(degrees).Radians); - Assert.AreEqual(degrees, Angle.FromDegrees(degrees).Degrees, 1E-6); + Assert.AreEqual(degrees, Angle.FromDegrees(degrees).Degrees); } [TestCase(1, 1)] @@ -122,10 +230,10 @@ public void FromRadians(double radians, double expected) Assert.AreEqual(expected, Angle.FromRadians(radians).Radians); } - [TestCase(20, 33, 49, 0.35890271998857842)] + [TestCase(20, 33, 49, 0.35890270667277291)] public void FromSexagesimal(int degrees, int minutes, double seconds, double expected) { - Assert.AreEqual(expected, Angle.FromSexagesimal(degrees, minutes, seconds).Radians, 1E-6); + Assert.AreEqual(expected, Angle.FromSexagesimal(degrees, minutes, seconds).Radians); } [TestCase("5 °", 5 * DegToRad)] diff --git a/src/Spatial/Euclidean/CoordinateSystem.cs b/src/Spatial/Euclidean/CoordinateSystem.cs index bba98ec..ed53dd1 100644 --- a/src/Spatial/Euclidean/CoordinateSystem.cs +++ b/src/Spatial/Euclidean/CoordinateSystem.cs @@ -257,12 +257,12 @@ public static CoordinateSystem Rotation(Angle angle, Vector3D v) public static CoordinateSystem Rotation(Angle yaw, Angle pitch, Angle roll) { var cs = new CoordinateSystem(); - var cosY = Math.Cos(yaw.Radians); - var sinY = Math.Sin(yaw.Radians); - var cosP = Math.Cos(pitch.Radians); - var sinP = Math.Sin(pitch.Radians); - var cosR = Math.Cos(roll.Radians); - var sinR = Math.Sin(roll.Radians); + var cosY = yaw.Cos; + var sinY = yaw.Sin; + var cosP = pitch.Cos; + var sinP = pitch.Sin; + var cosR = roll.Cos; + var sinR = roll.Sin; cs[0, 0] = cosY * cosP; cs[1, 0] = sinY * cosP; diff --git a/src/Spatial/Euclidean/Matrix2D.cs b/src/Spatial/Euclidean/Matrix2D.cs index 47026e1..8fa665b 100644 --- a/src/Spatial/Euclidean/Matrix2D.cs +++ b/src/Spatial/Euclidean/Matrix2D.cs @@ -16,8 +16,8 @@ public static class Matrix2D /// A transform matrix public static DenseMatrix Rotation(Angle rotation) { - double c = Math.Cos(rotation.Radians); - double s = Math.Sin(rotation.Radians); + var c = rotation.Cos; + var s = rotation.Sin; return Create(c, -s, s, c); } diff --git a/src/Spatial/Euclidean/Matrix3D.cs b/src/Spatial/Euclidean/Matrix3D.cs index fe42eae..e266b10 100644 --- a/src/Spatial/Euclidean/Matrix3D.cs +++ b/src/Spatial/Euclidean/Matrix3D.cs @@ -1,4 +1,3 @@ -using System; using MathNet.Numerics.LinearAlgebra; using MathNet.Numerics.LinearAlgebra.Double; using MathNet.Spatial.Units; @@ -20,10 +19,10 @@ public static Matrix RotationAroundXAxis(Angle angle) var rotationMatrix = new DenseMatrix(3, 3) { [0, 0] = 1, - [1, 1] = Math.Cos(angle.Radians), - [1, 2] = -Math.Sin(angle.Radians), - [2, 1] = Math.Sin(angle.Radians), - [2, 2] = Math.Cos(angle.Radians) + [1, 1] = angle.Cos, + [1, 2] = -angle.Sin, + [2, 1] = angle.Sin, + [2, 2] = angle.Cos }; return rotationMatrix; } @@ -37,11 +36,11 @@ public static Matrix RotationAroundYAxis(Angle angle) { var rotationMatrix = new DenseMatrix(3, 3) { - [0, 0] = Math.Cos(angle.Radians), - [0, 2] = Math.Sin(angle.Radians), + [0, 0] = angle.Cos, + [0, 2] = angle.Sin, [1, 1] = 1, - [2, 0] = -Math.Sin(angle.Radians), - [2, 2] = Math.Cos(angle.Radians) + [2, 0] = -angle.Sin, + [2, 2] = angle.Cos }; return rotationMatrix; } @@ -55,10 +54,10 @@ public static Matrix RotationAroundZAxis(Angle angle) { var rotationMatrix = new DenseMatrix(3, 3) { - [0, 0] = Math.Cos(angle.Radians), - [0, 1] = -Math.Sin(angle.Radians), - [1, 0] = Math.Sin(angle.Radians), - [1, 1] = Math.Cos(angle.Radians), + [0, 0] = angle.Cos, + [0, 1] = -angle.Sin, + [1, 0] = angle.Sin, + [1, 1] = angle.Cos, [2, 2] = 1 }; return rotationMatrix; @@ -123,9 +122,9 @@ public static Matrix RotationAroundArbitraryVector(UnitVector3D aboutVec var unitTensorProduct = aboutVector.GetUnitTensorProduct(); var crossProductMatrix = aboutVector.CrossProductMatrix; - var r1 = DenseMatrix.CreateIdentity(3).Multiply(Math.Cos(angle.Radians)); - var r2 = crossProductMatrix.Multiply(Math.Sin(angle.Radians)); - var r3 = unitTensorProduct.Multiply(1 - Math.Cos(angle.Radians)); + var r1 = DenseMatrix.CreateIdentity(3).Multiply(angle.Cos); + var r2 = crossProductMatrix.Multiply(angle.Sin); + var r3 = unitTensorProduct.Multiply(1 - angle.Cos); var totalR = r1.Add(r2).Add(r3); return totalR; } diff --git a/src/Spatial/Euclidean/Point2D.cs b/src/Spatial/Euclidean/Point2D.cs index 06fa71a..3483445 100644 --- a/src/Spatial/Euclidean/Point2D.cs +++ b/src/Spatial/Euclidean/Point2D.cs @@ -116,8 +116,8 @@ public static Point2D FromPolar(double radius, Angle angle) } return new Point2D( - radius * Math.Cos(angle.Radians), - radius * Math.Sin(angle.Radians)); + radius * angle.Cos, + radius * angle.Sin); } /// diff --git a/src/Spatial/Euclidean/Vector2D.cs b/src/Spatial/Euclidean/Vector2D.cs index 329e5a6..07170f9 100644 --- a/src/Spatial/Euclidean/Vector2D.cs +++ b/src/Spatial/Euclidean/Vector2D.cs @@ -53,7 +53,7 @@ public Vector2D(double x, double y) /// Gets the length of the vector /// [Pure] - public double Length => Math.Sqrt((X * X) + (Y * Y)); + public double Length => Math.Sqrt(X * X + Y * Y); /// /// Gets a vector orthogonal to this @@ -168,8 +168,8 @@ public static Vector2D FromPolar(double radius, Angle angle) } return new Vector2D( - radius * Math.Cos(angle.Radians), - radius * Math.Sin(angle.Radians)); + radius * angle.Cos, + radius * angle.Sin); } /// @@ -305,8 +305,7 @@ public bool IsPerpendicularTo(Vector2D other, double tolerance = 1e-10) public bool IsPerpendicularTo(Vector2D other, Angle tolerance) { var angle = AngleTo(other); - const double Perpendicular = Math.PI / 2; - return Math.Abs(angle.Radians - Perpendicular) < tolerance.Radians; + return (angle - Angle.HalfPi).Abs() < tolerance; } /// @@ -366,8 +365,8 @@ public Angle AngleTo(Vector2D other) return Angle.FromRadians( Math.Abs( Math.Atan2( - (X * other.Y) - (other.X * Y), - (X * other.X) + (Y * other.Y)))); + X * other.Y - other.X * Y, + X * other.X + Y * other.Y))); } /// @@ -378,10 +377,10 @@ public Angle AngleTo(Vector2D other) [Pure] public Vector2D Rotate(Angle angle) { - var cs = Math.Cos(angle.Radians); - var sn = Math.Sin(angle.Radians); - var x = (X * cs) - (Y * sn); - var y = (X * sn) + (Y * cs); + var cs = angle.Cos; + var sn = angle.Sin; + var x = X * cs - Y * sn; + var y = X * sn + Y * cs; return new Vector2D(x, y); } @@ -393,7 +392,7 @@ public Vector2D Rotate(Angle angle) [Pure] public double DotProduct(Vector2D other) { - return (X * other.X) + (Y * other.Y); + return X * other.X + Y * other.Y; } /// @@ -408,7 +407,7 @@ public double CrossProduct(Vector2D other) { // Though the cross product is undefined in 2D space, this is a useful mathematical operation to // determine angular direction and to compute the area of 2D shapes - return (X * other.Y) - (Y * other.X); + return X * other.Y - Y * other.X; } /// diff --git a/src/Spatial/Projective/Matrix3DHomogeneous.cs b/src/Spatial/Projective/Matrix3DHomogeneous.cs index 9bb020c..52a0a75 100644 --- a/src/Spatial/Projective/Matrix3DHomogeneous.cs +++ b/src/Spatial/Projective/Matrix3DHomogeneous.cs @@ -162,8 +162,8 @@ public static Matrix3DHomogeneous CreateScale(double sx, double sy, double sz) public static Matrix3DHomogeneous RotationAroundXAxis(Angle angle) { var result = new Matrix3DHomogeneous(); - var sinAngle = Math.Sin(angle.Radians); - var cosAngle = Math.Cos(angle.Radians); + var sinAngle = angle.Sin; + var cosAngle = angle.Cos; result.matrix[1, 1] = cosAngle; result.matrix[1, 2] = -sinAngle; result.matrix[2, 1] = sinAngle; @@ -179,8 +179,8 @@ public static Matrix3DHomogeneous RotationAroundXAxis(Angle angle) public static Matrix3DHomogeneous RotationAroundYAxis(Angle angle) { var result = new Matrix3DHomogeneous(); - var sinAngle = Math.Sin(angle.Radians); - var cosAngle = Math.Cos(angle.Radians); + var sinAngle = angle.Sin; + var cosAngle = angle.Cos; result.matrix[0, 0] = cosAngle; result.matrix[0, 2] = sinAngle; result.matrix[2, 0] = -sinAngle; @@ -196,8 +196,8 @@ public static Matrix3DHomogeneous RotationAroundYAxis(Angle angle) public static Matrix3DHomogeneous RotationAroundZAxis(Angle angle) { var result = new Matrix3DHomogeneous(); - var sinAngle = Math.Sin(angle.Radians); - var cosAngle = Math.Cos(angle.Radians); + var sinAngle = angle.Sin; + var cosAngle = angle.Cos; result.matrix[0, 0] = cosAngle; result.matrix[0, 1] = -sinAngle; result.matrix[1, 0] = sinAngle; @@ -281,10 +281,10 @@ public static Matrix3DHomogeneous TopView() public static Matrix3DHomogeneous Axonometric(Angle alpha, Angle beta) { var result = new Matrix3DHomogeneous(); - var sna = Math.Sin(alpha.Radians); - var cosAlpha = Math.Cos(alpha.Radians); - var sinBeta = Math.Sin(beta.Radians); - var cosBeta = Math.Cos(beta.Radians); + var sna = alpha.Sin; + var cosAlpha = alpha.Cos; + var sinBeta = beta.Sin; + var cosBeta = beta.Cos; result.matrix[0, 0] = cosBeta; result.matrix[0, 2] = sinBeta; result.matrix[1, 0] = sna * sinBeta; @@ -303,9 +303,9 @@ public static Matrix3DHomogeneous Axonometric(Angle alpha, Angle beta) public static Matrix3DHomogeneous Oblique(Angle alpha, Angle theta) { var result = new Matrix3DHomogeneous(); - var tanAlpha = Math.Tan(alpha.Radians); - var sinTheta = Math.Sin(theta.Radians); - var cosTheta = Math.Cos(theta.Radians); + var tanAlpha = alpha.Tan; + var sinTheta = theta.Sin; + var cosTheta = theta.Cos; result.matrix[0, 2] = -cosTheta / tanAlpha; result.matrix[1, 2] = -sinTheta / tanAlpha; result.matrix[2, 2] = 0; @@ -323,12 +323,12 @@ public static Matrix3DHomogeneous Euler(Angle alpha, Angle beta, Angle gamma) { var result = new Matrix3DHomogeneous(); - var sinAlpha = Math.Sin(alpha.Radians); - var cosAlpha = Math.Cos(alpha.Radians); - var sinBeta = Math.Sin(beta.Radians); - var cosBeta = Math.Cos(beta.Radians); - var sinGamma = Math.Sin(gamma.Radians); - var cosGamma = Math.Cos(gamma.Radians); + var sinAlpha = alpha.Sin; + var cosAlpha = alpha.Cos; + var sinBeta = beta.Sin; + var cosBeta = beta.Cos; + var sinGamma = gamma.Sin; + var cosGamma = gamma.Cos; result.matrix[0, 0] = (cosAlpha * cosGamma) - (sinAlpha * sinBeta * sinGamma); result.matrix[0, 1] = -sinBeta * sinGamma; diff --git a/src/Spatial/Units/Angle.cs b/src/Spatial/Units/Angle.cs index 7e9c575..8ba919c 100644 --- a/src/Spatial/Units/Angle.cs +++ b/src/Spatial/Units/Angle.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Xml; using System.Xml.Schema; @@ -22,12 +22,32 @@ public struct Angle : IComparable, IEquatable, IFormattable, IXmlS /// /// Conversion factor for converting Radians to Degrees /// - private const double RadToDeg = 180.0 / Math.PI; + private static readonly double RadToDeg = 180.0 / Math.PI; /// /// Conversion factor for converting Degrees to Radians /// - private const double DegToRad = Math.PI / 180.0; + private static readonly double DegToRad = Math.PI / 180.0; + + /// + /// The zero angle. + /// + public static readonly Angle Zero = new Angle(0); + + /// + /// The 90° angle. + /// + public static readonly Angle HalfPi = new Angle(Math.PI / 2); + + /// + /// The 180° angle. + /// + public static readonly Angle Pi = new Angle(Math.PI); + + /// + /// The 360° angle. + /// + public static readonly Angle TwoPi = new Angle(2 * Math.PI); /// /// Initializes a new instance of the struct. @@ -35,13 +55,80 @@ public struct Angle : IComparable, IEquatable, IFormattable, IXmlS /// The value in Radians private Angle(double radians) { - this.Radians = radians; + Radians = radians; + } + + /// + /// Returns the absolute of this angle. + /// + /// + public Angle Abs() + { + return new Angle(Math.Abs(Radians)); } /// /// Gets the value in degrees /// - public double Degrees => this.Radians * RadToDeg; + public double Degrees => Radians * RadToDeg; + + /// + /// Gets the cosine of this instance + /// + public double Cos => Math.Cos(Radians); + + /// Returns the angle whose cosine is the specified number. + /// A number representing a cosine, where must be greater than or equal to -1, but less than or equal to 1. + /// An angle, θ such that 0 ≤ θ ≤ π + public static Angle Acos(double d) + { + if (Math.Abs(d) > 1) + { + throw new ArgumentOutOfRangeException(nameof(d), "The cosine cannot be greater than 1 in magnitude"); + } + + return new Angle(Math.Acos(d)); + } + + /// + /// Gets the sine of this instance + /// + public double Sin => Math.Sin(Radians); + + /// Returns the angle whose sine is the specified number. + /// A number representing a sine, where must be greater than or equal to -1, but less than or equal to 1. + /// An angle, θ such that -π/2 ≤ θ ≤ π/2 + public static Angle Asin(double d) + { + if (Math.Abs(d) > 1) + { + throw new ArgumentOutOfRangeException(nameof(d), "The sine cannot be greater than 1 in magnitude"); + } + + return new Angle(Math.Asin(d)); + } + + /// + /// Gets the tangent of this instance + /// + public double Tan => Math.Tan(Radians); + + /// Returns the angle whose tangent is the specified number. + /// A number representing a tangent. + /// An angle, θ such that -π/2 ≤ θ ≤ π/2. + public static Angle Atan(double d) + { + return new Angle(Math.Atan(d)); + } + + /// Returns the angle whose tangent is the quotient of two specified numbers. + /// The y coordinate of a point. + /// The x coordinate of a point. + /// An angle, θ such that -π ≤ θ ≤ π, and tan(θ) = / , where (, ) is a point in the Cartesian plane + public static Angle Atan2(double y, double x) + { + return new Angle(Math.Atan2(y, x)); + } /// /// Returns a value that indicates whether two specified Angles are equal. @@ -264,7 +351,7 @@ public static Angle ReadFrom(XmlReader reader) /// public override string ToString() { - return this.ToString("G15", NumberFormatInfo.CurrentInfo); + return ToString("G15", NumberFormatInfo.CurrentInfo); } /// @@ -274,7 +361,7 @@ public override string ToString() /// The string representation of this instance. public string ToString(string format) { - return this.ToString(format, NumberFormatInfo.CurrentInfo); + return ToString(format, NumberFormatInfo.CurrentInfo); } /// @@ -284,13 +371,13 @@ public string ToString(string format) /// The string representation of this instance. public string ToString(IFormatProvider provider) { - return this.ToString("G15", NumberFormatInfo.GetInstance(provider)); + return ToString("G15", NumberFormatInfo.GetInstance(provider)); } /// public string ToString(string format, IFormatProvider provider) { - return this.ToString(format, provider, AngleUnit.Radians); + return ToString(format, provider, AngleUnit.Radians); } /// @@ -307,12 +394,12 @@ public string ToString(string format, IFormatProvider provider, T unit) if (unit == null || unit is Radians) { - return $"{this.Radians.ToString(format, provider)}\u00A0{unit?.ShortName ?? AngleUnit.Radians.ShortName}"; + return $"{Radians.ToString(format, provider)}\u00A0{unit?.ShortName ?? AngleUnit.Radians.ShortName}"; } if (unit is Degrees) { - return $"{this.Degrees.ToString(format, provider)}{unit.ShortName}"; + return $"{Degrees.ToString(format, provider)}{unit.ShortName}"; } throw new ArgumentOutOfRangeException(nameof(unit), unit, "Unknown unit"); @@ -321,7 +408,7 @@ public string ToString(string format, IFormatProvider provider, T unit) /// public int CompareTo(Angle value) { - return this.Radians.CompareTo(value.Radians); + return Radians.CompareTo(value.Radians); } /// @@ -334,7 +421,7 @@ public int CompareTo(Angle value) /// The maximum difference for being considered equal public bool Equals(Angle other, double tolerance) { - return Math.Abs(this.Radians - other.Radians) < tolerance; + return Math.Abs(Radians - other.Radians) < tolerance; } /// @@ -347,17 +434,17 @@ public bool Equals(Angle other, double tolerance) /// The maximum difference for being considered equal public bool Equals(Angle other, Angle tolerance) { - return Math.Abs(this.Radians - other.Radians) < tolerance.Radians; + return Math.Abs(Radians - other.Radians) < tolerance.Radians; } /// - public bool Equals(Angle other) => this.Radians.Equals(other.Radians); + public bool Equals(Angle other) => Radians.Equals(other.Radians); /// - public override bool Equals(object obj) => obj is Angle a && this.Equals(a); + public override bool Equals(object obj) => obj is Angle a && Equals(a); /// - public override int GetHashCode() => HashCode.Combine(this.Radians); + public override int GetHashCode() => HashCode.Combine(Radians); /// XmlSchema IXmlSerializable.GetSchema()