diff --git a/Hypercube.Shared.Math/Angle.cs b/Hypercube.Shared.Math/Angle.cs index e52a056..48f5f4b 100644 --- a/Hypercube.Shared.Math/Angle.cs +++ b/Hypercube.Shared.Math/Angle.cs @@ -1,16 +1,120 @@ using System.Runtime.CompilerServices; +using Hypercube.Shared.Math.Extensions; +using Hypercube.Shared.Math.Vector; namespace Hypercube.Shared.Math; -public readonly struct Angle(double theta) +public readonly struct Angle : IEquatable, IEquatable { public static readonly Angle Zero = new(0); + + public readonly double Theta; + + public double Degrees + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Theta * HyperMath.RadiansToDegrees; + } + + public Vector2 Vector + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new((float)System.Math.Cos(Theta), (float)System.Math.Sin(Theta)); + } + + public Angle(double theta) + { + Theta = theta; + } + + public Angle(Vector2 vector2) + { + vector2 = vector2.Normalized; + Theta = System.Math.Atan2(vector2.X, vector2.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Angle other) + { + return Theta.AboutEquals(other.Theta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(double other) + { + return Theta.AboutEquals(other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + return (obj is double theta && Equals(theta)) || + (obj is Angle angle && Equals(angle)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + return Theta.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() + { + return $"{Degrees} deg"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Angle a, double b) + { + return a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Angle a, double b) + { + return !a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Angle a, Angle b) + { + return a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Angle a, Angle b) + { + return !a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(Angle angle) + { + return angle.Theta; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Angle(double radians) + { + return new Angle(radians); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Angle(float radians) + { + return new Angle(radians); + } - public readonly double Theta = theta; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Angle FromDegrees(double degrees) + { + return new Angle(degrees * HyperMath.DegreesToRadians); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator float(Angle angle) + public static Angle FromDegrees(float degrees) { - return (float)angle.Theta; + return new Angle(degrees * HyperMath.DegreesToRadians); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs b/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs index ac59f56..14cb0ac 100644 --- a/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs +++ b/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs @@ -2,15 +2,15 @@ public static class FloatingPointEqualsExtension { - public static bool AboutEquals(this double a, double b) + public static bool AboutEquals(this double a, double b, double tolerance = 1E-15d) { var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * 1E-15d; return System.Math.Abs(a - b) <= epsilon; } - public static bool AboutEquals(this float a, float b) + public static bool AboutEquals(this float a, float b, float tolerance = 1E-15f) { - var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * 1E-15f; + var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * tolerance ; return System.Math.Abs(a - b) <= epsilon; } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/HyperMath.cs b/Hypercube.Shared.Math/HyperMath.cs index ab118a4..de5bb5b 100644 --- a/Hypercube.Shared.Math/HyperMath.cs +++ b/Hypercube.Shared.Math/HyperMath.cs @@ -2,7 +2,15 @@ public static class HyperMath { - // ReSharper disable once InconsistentNaming - public const float PI = MathF.PI; - public const float PiOver2 = PI / 2; + public const double PI = System.Math.PI; + + public const double PIOver2 = PI / 2; + public const double PIOver4 = PI / 4; + public const double PIOver6 = PI / 6; + + public const double TwoPI = 2 * PI; + public const double ThreePiOver2 = 3 * PI / 2; + + public const double RadiansToDegrees = 180 / PI; + public const double DegreesToRadians = PI / 180; } \ No newline at end of file diff --git a/Hypercube.Shared.Math/HyperMathF.cs b/Hypercube.Shared.Math/HyperMathF.cs new file mode 100644 index 0000000..d11822e --- /dev/null +++ b/Hypercube.Shared.Math/HyperMathF.cs @@ -0,0 +1,16 @@ +namespace Hypercube.Shared.Math; + +public static class HyperMathF +{ + public const float PI = MathF.PI; + + public const float PIOver2 = PI / 2; + public const float PIOver4 = PI / 4; + public const float PIOver6 = PI / 6; + + public const float TwoPI = 2 * PI; + public const float ThreePiOver2 = 3 * PI / 2; + + public const float RadiansToDegrees = 180 / PI; + public const float DegreesToRadians = PI / 180; +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs index a2ca47c..49fc631 100644 --- a/Hypercube.Shared.Math/Quaternion.cs +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -215,7 +215,7 @@ public static Vector3 ToEuler(Quaternion quaternion) // Singularity at north pole return new Vector3( 0, - HyperMath.PiOver2, + HyperMathF.PIOver2, 2f * MathF.Atan2(quaternion.X, quaternion.W) ); @@ -223,7 +223,7 @@ public static Vector3 ToEuler(Quaternion quaternion) // Singularity at south pole return new Vector3( 0, - -HyperMath.PiOver2, + -HyperMathF.PIOver2, -2f * MathF.Atan2(quaternion.X, quaternion.W) ); diff --git a/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs index 8d6230d..d906155 100644 --- a/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs +++ b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs @@ -12,6 +12,6 @@ public partial struct Transform2 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Transform3(Transform2 transform2) { - return new Transform3(transform2.Position, new Quaternion(Vector3.UnitZ * transform2.Rotation), transform2.Scale); + return new Transform3(transform2.Position, new Quaternion(Vector3.UnitZ * (float)transform2.Rotation), transform2.Scale); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform2.cs b/Hypercube.Shared.Math/Transform/Transform2.cs index 5dcf94a..c5d4674 100644 --- a/Hypercube.Shared.Math/Transform/Transform2.cs +++ b/Hypercube.Shared.Math/Transform/Transform2.cs @@ -56,7 +56,7 @@ public Transform2 SetScale(Vector2 scale) private void UpdateMatrix() { Matrix = Matrix4X4.CreateTranslation(Position) * - Matrix4X4.CreateRotationZ(Rotation) * + Matrix4X4.CreateRotationZ((float)Rotation) * Matrix4X4.CreateScale(Scale); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Vector/Vector2.cs b/Hypercube.Shared.Math/Vector/Vector2.cs index 392d3b0..da55088 100644 --- a/Hypercube.Shared.Math/Vector/Vector2.cs +++ b/Hypercube.Shared.Math/Vector/Vector2.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Hypercube.Shared.Math.Extensions; namespace Hypercube.Shared.Math.Vector; @@ -33,6 +34,12 @@ public float Length get => MathF.Sqrt(LengthSquared); } + public Vector2 Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; + } + public Vector2(float x, float y) { X = x; @@ -62,7 +69,8 @@ public Vector2 WithY(float value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Vector2 other) { - return X.Equals(other.X) && Y.Equals(other.Y); + return X.AboutEquals(other.X) && + Y.AboutEquals(other.Y); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Hypercube.Shared.Math/Vector/Vector2Int.cs b/Hypercube.Shared.Math/Vector/Vector2Int.cs index 3b7b8ff..91c7a2b 100644 --- a/Hypercube.Shared.Math/Vector/Vector2Int.cs +++ b/Hypercube.Shared.Math/Vector/Vector2Int.cs @@ -32,6 +32,12 @@ public float Length [MethodImpl(MethodImplOptions.AggressiveInlining)] get => MathF.Sqrt(LengthSquared); } + + public Vector2Int Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; + } public Vector2Int(int x, int y) { diff --git a/Hypercube.UnitTests/Math/AngleTest.cs b/Hypercube.UnitTests/Math/AngleTest.cs new file mode 100644 index 0000000..8ebaaab --- /dev/null +++ b/Hypercube.UnitTests/Math/AngleTest.cs @@ -0,0 +1,53 @@ +using Hypercube.Shared.Math; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.UnitTests.Math; + +public static class AngleTest +{ + [Test] + public static void Degrees() + { + Assert.Multiple(() => + { + Assert.That(new Angle(HyperMath.PI).Degrees, Is.EqualTo(180d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver2).Degrees, Is.EqualTo(90d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver4).Degrees, Is.EqualTo(45d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver6).Degrees, Is.EqualTo(30d).Within(0.01d)); + + Assert.That(Angle.FromDegrees(180d), Is.EqualTo(new Angle(HyperMath.PI))); + Assert.That(Angle.FromDegrees(90d), Is.EqualTo(new Angle(HyperMath.PIOver2))); + Assert.That(Angle.FromDegrees(45d), Is.EqualTo(new Angle(HyperMath.PIOver4))); + Assert.That(Angle.FromDegrees(30d), Is.EqualTo(new Angle(HyperMath.PIOver6))); + }); + Assert.Pass($"{nameof(Angle)} degrees passed"); + } + + [Test] + public static void Vector() + { + Assert.Multiple(() => + { + Assert.That(Angle.Zero.GetRoundVector(), Is.EqualTo(Vector2.UnitX)); + Assert.That(new Angle(HyperMath.PI).GetRoundVector(), Is.EqualTo(-Vector2.UnitX)); + Assert.That(new Angle(-HyperMath.PI).GetRoundVector(), Is.EqualTo(-Vector2.UnitX)); + + Assert.That(new Angle(HyperMath.PIOver2).GetRoundVector(), Is.EqualTo(Vector2.UnitY)); + Assert.That(new Angle(HyperMath.ThreePiOver2).GetRoundVector(), Is.EqualTo(-Vector2.UnitY)); + Assert.That(new Angle(-HyperMath.PIOver2).GetRoundVector(), Is.EqualTo(-Vector2.UnitY)); + + Assert.That(new Angle(HyperMath.PIOver4).GetRoundVector(), Is.EqualTo(Vector2.One.Normalized)); + Assert.That(new Angle(HyperMath.ThreePiOver2 - HyperMath.PIOver4).GetRoundVector(), Is.EqualTo(-Vector2.One.Normalized)); + }); + + Assert.Pass($"{nameof(Angle)} vector passed"); + } + + private static Vector2 GetRoundVector(this Angle angle) + { + var vector = angle.Vector; + var x = MathF.Abs(vector.X) < 1e-15f ? 0 : vector.X; + var y = MathF.Abs(vector.Y) < 1e-15f ? 0 : vector.Y; + return new Vector2(x, y); + } +} \ No newline at end of file diff --git a/Hypercube.UnitTests/Math/FloatingPointTest.cs b/Hypercube.UnitTests/Math/FloatingPointTest.cs index 7b1d5f4..dd14565 100644 --- a/Hypercube.UnitTests/Math/FloatingPointTest.cs +++ b/Hypercube.UnitTests/Math/FloatingPointTest.cs @@ -7,8 +7,11 @@ public class FloatingPointTest [Test] public void Equals() { - Assert.That((0.1d + 0.2d).AboutEquals(0.3d)); - Assert.That((0.1f + 0.2f).AboutEquals(0.3f)); + Assert.Multiple(() => + { + Assert.That((0.1d + 0.2d).AboutEquals(0.3d)); + Assert.That((0.1f + 0.2f).AboutEquals(0.3f)); + }); Assert.Pass($"{nameof(FloatingPointEqualsExtension)} passed"); }