using Sandbox; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using Sandbox.Interpolation; /// /// Euler angles. Unlike a Rotation, Euler angles can represent multiple revolutions (rotations) around an axis, /// but suffer from issues like gimbal lock and lack of a defined "up" vector. Use Rotation for most cases. /// [JsonConverter( typeof( Sandbox.Internal.JsonConvert.AnglesConverter ) )] [StructLayout( LayoutKind.Sequential )] public struct Angles : IEquatable, IParsable, IInterpolator { /// /// The pitch component, typically up/down. /// public float pitch; /// /// The yaw component, typically left/right. /// public float yaw; /// /// The roll component, basically rotation around the axis. /// public float roll; /// /// Initializes the angles object with given components. /// /// The Pitch component. /// The Yaw component. /// The roll component. [ActionGraphNode( "angles.new" ), Title( "Euler Angles" ), Group( "Math/Geometry/Angles" )] public Angles( float pitch, float yaw, float roll ) { this.pitch = pitch; this.yaw = yaw; this.roll = roll; } /// /// Copies values of given angles object. /// public Angles( Angles other ) { this.pitch = other.pitch; this.yaw = other.yaw; this.roll = other.roll; } /// /// Where x, y and z represent the pitch, yaw and roll respectively. /// public Angles( Vector3 vector ) { this.pitch = vector.x; this.yaw = vector.y; this.roll = vector.z; } /// /// Initializes the angles object with all components set to given value. /// public Angles( float all = 0.0f ) : this( all, all, all ) { } /// /// Converts these Euler angles to a rotation. The angles will be normalized. /// /// public readonly Rotation ToRotation() { return Rotation.From( this ); } /// /// Return as a Vector3, where x = pitch etc /// public readonly Vector3 AsVector3() { return new Vector3( pitch, yaw, roll ); } public readonly override string ToString() { return $"Pitch = {pitch:0.00}, Yaw = {yaw:0.00}, Roll = {roll:0.00}"; } /// /// An angle constant that has all its values set to 0. Use this instead of making a static 0,0,0 object yourself. /// public static readonly Angles Zero = new( 0 ); /// /// Returns the angles of a uniformly random rotation. /// [ActionGraphNode( "angles.random" ), Title( "Random Angles" ), Group( "Math/Geometry/Angles" )] public static Angles Random => Rotation.Random.Angles(); /// /// Returns true if this angles object's components are all nearly zero with given tolerance. /// public readonly bool IsNearlyZero( double tolerance = 0.000001 ) { return MathF.Abs( pitch ) <= tolerance && MathF.Abs( yaw ) <= tolerance && MathF.Abs( roll ) <= tolerance; } /// /// Returns this angles object with given pitch component. /// public readonly Angles WithPitch( float pitch ) => new Angles( pitch, yaw, roll ); /// /// Returns this angles object with given yaw component. /// public readonly Angles WithYaw( float yaw ) => new Angles( pitch, yaw, roll ); /// /// Returns this angles object with given roll component. /// public readonly Angles WithRoll( float roll ) => new Angles( pitch, yaw, roll ); /// /// Given a string, try to convert this into an angles object. The format is "p,y,r". /// public static Angles Parse( string str ) { if ( TryParse( str, null, out var res ) ) return res; return default; } /// public static Angles Parse( string str, IFormatProvider provider ) { return Parse( str ); } /// public static bool TryParse( string str, out Angles result ) { return TryParse( str, CultureInfo.InvariantCulture, out result ); } /// public static bool TryParse( [NotNullWhen( true )] string str, IFormatProvider provider, [MaybeNullWhen( false )] out Angles result ) { result = Angles.Zero; if ( string.IsNullOrWhiteSpace( str ) ) return false; str = str.Trim( '[', ']', ' ', '\n', '\r', '\t', '"' ); var components = str.Split( new[] { ' ', ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries ); if ( components.Length != 3 ) return false; if ( !float.TryParse( components[0], NumberStyles.Float, provider, out float p ) || !float.TryParse( components[1], NumberStyles.Float, provider, out float y ) || !float.TryParse( components[2], NumberStyles.Float, provider, out float r ) ) { return false; } result = new Angles( p, y, r ); return true; } /// /// Returns clamped version of this object, meaning the angle on each axis is transformed to range of [0,360). /// public readonly Angles Clamped() { return new Angles( ClampAngle( pitch ), ClampAngle( yaw ), ClampAngle( roll ) ); } /// /// Returns normalized version of this object, meaning the angle on each axis is normalized to range of (-180,180]. /// public readonly Angles Normal => new Angles( NormalizeAngle( pitch ), NormalizeAngle( yaw ), NormalizeAngle( roll ) ); /// /// Clamps the angle to range of [0, 360) /// public static float ClampAngle( float v ) { v %= 360.0f; return (v < 0.0f) ? v + 360.0f : v; } /// /// Normalizes the angle to range of (-180, 180] /// public static float NormalizeAngle( float v ) { v = ClampAngle( v ); return (v > 180.0f) ? v - 360.0f : v; } /// /// Performs linear interpolation on the two given angle objects. /// /// Angle A /// Angle B /// Fraction in range [0,1] between the 2 angle objects to use for interpolation. public static Angles Lerp( in Angles source, in Angles target, float frac ) { return source + (target - source).Normal * frac; } /// /// Performs linear interpolation on the two given angle objects. /// /// Angle B /// Fraction in range [0,1] between the 2 angle objects to use for interpolation. public readonly Angles LerpTo( Angles target, float frac ) => Lerp( this, target, frac ); /// /// Converts an angle to a forward vector. /// public static Vector3 AngleVector( Angles ang ) { const float piOver180 = (float)(Math.PI / 180.0); float[] vAngles = { ang.yaw, ang.pitch }; float[] vSines = new float[2]; float[] vCosines = new float[2]; vSines[0] = (float)Math.Sin( vAngles[0] * piOver180 ); vSines[1] = (float)Math.Sin( vAngles[1] * piOver180 ); vCosines[0] = (float)Math.Cos( vAngles[0] * piOver180 ); vCosines[1] = (float)Math.Cos( vAngles[1] * piOver180 ); return new Vector3( vCosines[1] * vCosines[0], vCosines[1] * vSines[0], -vSines[1] ); } /// /// The forward direction vector for this angle. /// public Vector3 Forward { readonly get { return AngleVector( this ); } set { this = Vector3.VectorAngle( value ); } } /// /// Snap to grid /// public readonly Angles SnapToGrid( float gridSize, bool sx = true, bool sy = true, bool sz = true ) { return new Angles( sx ? pitch.SnapToGrid( gridSize ) : pitch, sy ? yaw.SnapToGrid( gridSize ) : yaw, sz ? roll.SnapToGrid( gridSize ) : roll ); } #region operators public static Angles operator +( Angles c1, Angles c2 ) { return new Angles( c1.pitch + c2.pitch, c1.yaw + c2.yaw, c1.roll + c2.roll ); } public static Angles operator +( Angles c1, Vector3 c2 ) { return new Angles( c1.pitch + c2.x, c1.yaw + c2.y, c1.roll + c2.z ); } public static Angles operator -( Angles c1, Angles c2 ) { return new Angles( c1.pitch - c2.pitch, c1.yaw - c2.yaw, c1.roll - c2.roll ); } public static Angles operator *( Angles c1, float c2 ) => new Angles( c1.pitch * c2, c1.yaw * c2, c1.roll * c2 ); public static Angles operator /( Angles c1, float c2 ) => new Angles( c1.pitch / c2, c1.yaw / c2, c1.roll / c2 ); #endregion #region equality public static bool operator ==( in Angles left, in Angles right ) => left.Equals( right ); public static bool operator !=( in Angles left, in Angles right ) => !(left == right); public override readonly bool Equals( object obj ) => obj is Angles o && Equals( o ); public readonly bool Equals( Angles o ) => (pitch, yaw, roll) == (o.pitch, o.yaw, o.roll); public readonly override int GetHashCode() => HashCode.Combine( pitch, yaw, roll ); #endregion Angles IInterpolator.Interpolate( Angles a, Angles b, float delta ) { return a.LerpTo( b, delta ); } }