using Sandbox; using Sandbox.Interpolation; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json.Serialization; /// /// A 2-dimensional vector. Typically represents a position, size, or direction in 2D space. /// [JsonConverter( typeof( Sandbox.Internal.JsonConvert.Vector2Converter ) )] [StructLayout( LayoutKind.Sequential )] public partial struct Vector2 : System.IEquatable, IParsable, IInterpolator { internal System.Numerics.Vector2 _vec; /// /// X component of this vector. /// [ActionGraphInclude( AutoExpand = true )] public float x { [MethodImpl( MethodImplOptions.AggressiveInlining )] readonly get => _vec.X; [MethodImpl( MethodImplOptions.AggressiveInlining )] set => _vec.X = value; } /// /// Y component of this vector. /// [ActionGraphInclude( AutoExpand = true )] public float y { [MethodImpl( MethodImplOptions.AggressiveInlining )] readonly get => _vec.Y; [MethodImpl( MethodImplOptions.AggressiveInlining )] set => _vec.Y = value; } /// /// Initializes a 2D vector with given components. /// /// The X component. /// The Y component. [ActionGraphNode( "vec2.new" ), Title( "Vector2" ), Group( "Math/Geometry/Vector2" )] [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( float x, float y ) : this( new System.Numerics.Vector2( x, y ) ) { } /// /// Initializes a Vector2 from a given Vector2, i.e. creating a copy. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( in Vector2 other ) : this( other.x, other.y ) { } /// /// Initializes the 2D vector with all components set to the given value. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( float all ) : this( all, all ) { } /// /// Initializes the 2D vector with components from given 3D Vector, discarding the Z component. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( Vector3 v ) : this( new System.Numerics.Vector2( (float)v.x, (float)v.y ) ) { } /// /// Initializes the 2D vector with components from given 4D vector, discarding the Z and W components. /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( Vector4 v ) : this( new System.Numerics.Vector2( (float)v.x, (float)v.y ) ) { } [MethodImpl( MethodImplOptions.AggressiveInlining )] public Vector2( System.Numerics.Vector2 v ) { _vec = v; } /// /// Returns a 2D vector with every component set to 1 /// public static Vector2 One { get; } = new Vector2( 1 ); /// /// Returns a 2D vector with every component set to 0 /// public static Vector2 Zero { get; } = new Vector2( 0 ); /// /// Returns a 2D vector with Y set to -1. This typically represents up in 2D space. /// public static Vector2 Up { get; } = new Vector2( 0, -1 ); /// /// Returns a 2D vector with Y set to 1. This typically represents down in 2D space. /// public static Vector2 Down { get; } = new Vector2( 0, 1 ); /// /// Returns a 2D vector with X set to -1. This typically represents the left hand direction in 2D space. /// public static Vector2 Left { get; } = new Vector2( -1, 0 ); /// /// Returns a 2D vector with X set to 1. This typically represents the right hand direction in 2D space. /// public static Vector2 Right { get; } = new Vector2( 1, 0 ); /// /// Uniformly samples a 2D position from all points with distance at most 1 from the origin. /// [ActionGraphNode( "vec2.random" ), Title( "Random Vector2" ), Group( "Math/Geometry/Vector2" ), Icon( "casino" )] public static Vector2 Random => SandboxSystem.Random.VectorInCircle(); /// /// Returns a point on a circle at given rotation from X axis, counter clockwise. /// [ActionGraphNode( "vec2.fromrads" ), Pure, Title( "Vector2 From Radians" ), Group( "Math/Geometry/Vector2" ), Icon( "architecture" )] public static Vector2 FromRadians( float radians ) => new Vector2( MathF.Sin( radians ), -MathF.Cos( radians ) ); /// /// Returns a point on a circle at given rotation from X axis, counter clockwise. /// [ActionGraphNode( "vec2.fromdegs" ), Pure, Title( "Vector2 From Degrees" ), Group( "Math/Geometry/Vector2" ), Icon( "architecture" )] public static Vector2 FromDegrees( float degrees ) => FromRadians( degrees.DegreeToRadian() ); /// /// Return the same vector but with a length of one /// [JsonIgnore] public readonly Vector2 Normal => IsNearZeroLength ? Vector2.Zero : System.Numerics.Vector2.Normalize( _vec ); /// /// Returns the magnitude of the vector /// [JsonIgnore] public readonly float Length => _vec.Length(); /// /// This is faster than Length, so is better to use in certain circumstances /// [JsonIgnore] public readonly float LengthSquared => _vec.LengthSquared(); /// /// Returns the inverse of this vector, which is useful for scaling vectors /// [JsonIgnore] public readonly Vector2 Inverse => new( 1.0f / x, 1.0f / y ); /// /// Return the angle of this vector in degrees, always between 0 and 360 /// [JsonIgnore] public readonly float Degrees => System.MathF.Atan2( x, -y ).RadianToDegree().NormalizeDegrees(); /// /// Returns a vector that runs perpendicular to this one /// [JsonIgnore] public readonly Vector2 Perpendicular => new Vector2( -y, x ); /// /// Returns true if x, y, or z are NaN /// [JsonIgnore] public readonly bool IsNaN => float.IsNaN( x ) || float.IsNaN( y ); /// /// Returns true if x, y, or z are infinity /// [JsonIgnore] public readonly bool IsInfinity => float.IsInfinity( x ) || float.IsInfinity( y ); /// /// Returns true if the squared length is less than 1e-8 (which is really near zero) /// [JsonIgnore] public readonly bool IsNearZeroLength => LengthSquared <= 1e-8; /// /// Return this vector with given X. /// public readonly Vector2 WithX( float x ) => new Vector2( x, y ); /// /// Return this vector with given Y. /// public readonly Vector2 WithY( float y ) => new Vector2( x, y ); /// /// Returns true if value on every axis is less than tolerance away from zero /// public readonly bool IsNearlyZero( float tolerance = 0.0001f ) { var abs = System.Numerics.Vector2.Abs( _vec ); return abs.X < tolerance && abs.Y < tolerance; } /// /// Returns a vector whose length is limited to given maximum /// public readonly Vector2 ClampLength( float maxLength ) { if ( LengthSquared <= 0 ) return Zero; if ( LengthSquared < (maxLength * maxLength) ) return this; return Normal * maxLength; } /// /// Returns a vector whose length is limited between given minimum and maximum /// public readonly Vector2 ClampLength( float minLength, float maxLength ) { float minSqr = minLength * minLength; float maxSqr = maxLength * maxLength; float lenSqr = LengthSquared; if ( lenSqr <= 0.0f ) return Zero; if ( lenSqr <= minSqr ) return Normal * minLength; if ( lenSqr >= maxSqr ) return Normal * maxLength; return this; } /// /// Returns a vector each axis of which is clamped to between the 2 given vectors. Basically clamps a point to a square. /// /// The mins vector. Values on each axis should be smaller than those of the maxs vector. See Vector2.Sort. /// The maxs vector. Values on each axis should be larger than those of the mins vector. See Vector2.Sort. public readonly Vector2 Clamp( Vector2 otherMin, Vector2 otherMax ) { return new Vector2( Math.Clamp( x, otherMin.x, otherMax.x ), Math.Clamp( y, otherMin.y, otherMax.y ) ); } /// /// Returns a vector each axis of which is clamped to given min and max values. /// /// Minimum value for each axis. /// Maximum value for each axis. public readonly Vector2 Clamp( float min, float max ) => Clamp( new Vector2( min ), new Vector2( max ) ); /// /// Restricts a vector between a minimum and maximum value. /// /// The vector to restrict. /// The mins vector. Values on each axis should be smaller than those of the maxs vector. See Vector2.Sort. /// The maxs vector. Values on each axis should be larger than those of the mins vector. See Vector2.Sort. /// public static Vector2 Clamp( in Vector2 value, in Vector2 min, in Vector2 max ) => System.Numerics.Vector2.Clamp( value, min, max ); /// /// Returns a vector that has the minimum values on each axis between this vector and given vector. /// public readonly Vector2 ComponentMin( Vector2 other ) { return System.Numerics.Vector2.Min( _vec, other._vec ); } /// /// Returns a vector that has the minimum values on each axis between the 2 given vectors. /// public static Vector2 Min( Vector2 a, Vector2 b ) => a.ComponentMin( b ); /// /// Returns a vector that has the maximum values on each axis between this vector and given vector. /// public readonly Vector2 ComponentMax( Vector2 other ) { return System.Numerics.Vector2.Max( _vec, other._vec ); } /// /// Returns a vector that has the maximum values on each axis between the 2 given vectors. /// public static Vector2 Max( Vector2 a, Vector2 b ) => a.ComponentMax( b ); /// /// Linearly interpolate from point a to point b. /// [ActionGraphNode( "geom.lerp" ), Pure, Group( "Math/Geometry" ), Icon( "timeline" )] public static Vector2 Lerp( Vector2 a, Vector2 b, [Range( 0f, 1f )] float frac, bool clamp = true ) { if ( clamp ) frac = frac.Clamp( 0.0f, 1.0f ); return System.Numerics.Vector2.Lerp( a._vec, b._vec, frac ); } /// /// Linearly interpolate from this vector to given vector. /// public readonly Vector2 LerpTo( Vector2 target, float t, bool clamp = true ) { return Lerp( this, target, t, clamp ); } /// /// Linearly interpolate from point a to point b with separate fraction for each axis. /// public static Vector2 Lerp( Vector2 a, Vector2 b, Vector2 t, bool clamp = true ) { if ( clamp ) t = t.Clamp( 0.0f, 1.0f ); return System.Numerics.Vector2.Lerp( a._vec, b._vec, t ); } /// /// Linearly interpolate from this vector to given vector with separate fraction for each axis. /// public readonly Vector2 LerpTo( Vector2 target, Vector2 t, bool clamp = true ) { return Lerp( this, target, t, clamp ); } /// /// Remaps from one range to another. /// internal readonly Vector2 Remap( Rect oldRange, Rect newRange, bool clamp ) { return new Vector2( x.Remap( oldRange.Left, oldRange.Right, newRange.Left, newRange.Right, clamp ), y.Remap( oldRange.Top, oldRange.Bottom, newRange.Top, newRange.Bottom, clamp ) ); } /// /// Returns the scalar/dot product between the 2 given vectors. /// [ActionGraphNode( "geom.dot" ), Pure, Group( "Math/Geometry" ), Icon( "fiber_manual_record" )] public static float Dot( Vector2 a, Vector2 b ) { return System.Numerics.Vector2.Dot( a._vec, b._vec ); } /// /// Returns the scalar/dot product between this and the given vector. /// public readonly float Dot( in Vector2 b ) => Dot( this, b ); /// /// Returns distance between the 2 given vectors. /// public static float DistanceBetween( Vector2 a, Vector2 b ) => System.Numerics.Vector2.Distance( a._vec, b._vec ); /// /// Returns distance between the 2 given vectors. /// [ActionGraphNode( "geom.dist" ), Pure, Title( "Distance" ), Group( "Math/Geometry" ), Icon( "straighten" )] public static float Distance( in Vector2 a, in Vector2 b ) => System.Numerics.Vector2.Distance( a._vec, b._vec ); /// /// Returns distance between this and given vectors. /// public readonly float Distance( Vector2 target ) => DistanceBetween( this, target ); /// /// Returns squared distance between the 2 given vectors. This is faster than DistanceBetween, /// and can be used for things like comparing distances, as long as only squared values are used. /// public static float DistanceBetweenSquared( Vector2 a, Vector2 b ) { return (b - a).LengthSquared; } /// /// Returns squared distance between the 2 given vectors. This is faster than DistanceBetween, /// and can be used for things like comparing distances, as long as only squared values are used. /// public static float DistanceSquared( in Vector2 a, in Vector2 b ) => System.Numerics.Vector2.DistanceSquared( a._vec, b._vec ); /// /// Returns squared distance between the 2 given vectors. This is faster than Distance, /// and can be used for things like comparing distances, as long as only squared values are used. /// public readonly float DistanceSquared( Vector2 target ) => DistanceBetweenSquared( this, target ); /// /// Calculates the normalized direction vector from one point to another in 2D space. /// public static Vector2 Direction( in Vector2 from, in Vector2 to ) { return (to - from).Normal; } /// /// Given a vector like 1,1,1 and direction 1,0,0, will return 0,1,1. /// This is useful for velocity collision type events, where you want to /// cancel out velocity based on a normal. /// For this to work properly, direction should be a normal, but you can scale /// how much you want to subtract by scaling the direction. Ie, passing in a direction /// with a length of 0.5 will remove half the direction. /// public readonly Vector2 SubtractDirection( in Vector2 direction, float strength = 1.0f ) { return this - (direction * Dot( direction ) * strength); } /// /// Returns a new vector whos length is closer to given target length by given amount. /// public readonly Vector2 Approach( float length, float amount ) { return Normal * Length.Approach( length, amount ); } /// /// Returns a new vector with all values positive. -5 becomes 5, etc. /// public readonly Vector2 Abs() { return System.Numerics.Vector2.Abs( _vec ); } /// /// Returns a new vector with all values positive. -5 becomes 5, etc. /// public static Vector2 Abs( in Vector2 value ) { return System.Numerics.Vector2.Abs( value._vec ); } /// /// Returns a reflected vector based on incoming direction and plane normal. Like a ray reflecting off of a mirror. /// public static Vector2 Reflect( in Vector2 direction, in Vector2 normal ) { return System.Numerics.Vector2.Reflect( direction._vec, normal._vec ); } /// /// Sort these two vectors into min and max. This doesn't just swap the vectors, it sorts each component. /// So that min will come out containing the minimum x and y values. /// public static void Sort( ref Vector2 min, ref Vector2 max ) { var a = new Vector2( Math.Min( min.x, max.x ), Math.Min( min.y, max.y ) ); var b = new Vector2( Math.Max( min.x, max.x ), Math.Max( min.y, max.y ) ); min = a; max = b; } /// /// Returns true if we're nearly equal to the passed vector. /// /// The value to compare with /// The max difference between component values /// True if nearly equal public readonly bool AlmostEqual( Vector2 v, float delta = 0.0001f ) { if ( Math.Abs( x - v.x ) > delta ) return false; if ( Math.Abs( y - v.y ) > delta ) return false; return true; } /// /// Calculates position of a point on a cubic bezier curve at given fraction. /// /// Point A of the curve. /// Point B of the curve. /// Tangent for the Point A. /// Tangent for the Point B. /// How far along the path to get a point on. Range is 0 to 1, inclusive. /// The point on the curve public static Vector2 CubicBezier( in Vector2 source, in Vector2 target, in Vector2 sourceTangent, in Vector2 targetTangent, float t ) { t = t.Clamp( 0, 1 ); var invT = 1 - t; return invT * invT * invT * source + 3 * invT * invT * t * sourceTangent + 3 * invT * t * t * targetTangent + t * t * t * target; } /// /// Snap to grid along all 3 axes. /// public readonly Vector2 SnapToGrid( float gridSize, bool sx = true, bool sy = true ) { return new Vector2( sx ? x.SnapToGrid( gridSize ) : x, sy ? y.SnapToGrid( gridSize ) : y ); } /// /// Returns the distance between two direction vectors in degrees. /// public static float GetAngle( in Vector2 v1, in Vector2 v2 ) { return MathF.Acos( Dot( v1.Normal, v2.Normal ).Clamp( -1, 1 ) ).RadianToDegree(); } /// /// Returns the distance between this vector and another in degrees. /// public readonly float Angle( in Vector2 other ) { return GetAngle( this, other ); } /// /// Try to add to this vector. If we're already over max then don't add. /// If we're over max when we add, clamp in that direction so we're not. /// public readonly Vector2 AddClamped( in Vector2 toAdd, float maxLength ) { var dir = toAdd.Normal; // Already over - just return self var dot = Dot( dir ); if ( dot > maxLength ) return this; // Add it var vec = this + toAdd; dot = vec.Dot( dir ); if ( dot < maxLength ) return vec; // We're over, take off the rest vec -= dir * (dot - maxLength); return vec; } /// /// Rotate this vector around given point by given angle in degrees and /// return the result as a new vector. /// /// /// /// public readonly Vector2 RotateAround( in Vector2 center, float angleDegrees ) { var radians = angleDegrees.DegreeToRadian(); var cos = MathF.Cos( radians ); var sin = MathF.Sin( radians ); var dx = x - center.x; var dy = y - center.y; return new Vector2( center.x + (dx * cos - dy * sin), center.y + (dx * sin + dy * cos) ); } #region operators public float this[int index] { [MethodImpl( MethodImplOptions.AggressiveInlining )] readonly get => _vec[index]; [MethodImpl( MethodImplOptions.AggressiveInlining )] set => _vec[index] = value; } [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator +( Vector2 c1, Vector2 c2 ) => c1._vec + c2._vec; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator -( Vector2 c1, Vector2 c2 ) => c1._vec - c2._vec; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator -( Vector2 c1 ) => System.Numerics.Vector2.Negate( c1._vec ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator *( Vector2 c1, float f ) => c1._vec * f; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator *( float f, Vector2 c1 ) => c1._vec * f; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator *( Vector2 c1, Vector2 c2 ) => c1._vec * c2._vec; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator /( Vector2 c1, Vector2 c2 ) => c1._vec / c2._vec; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static Vector2 operator /( Vector2 c1, float c2 ) => c1._vec / c2; [MethodImpl( MethodImplOptions.AggressiveInlining )] public static implicit operator Vector2( System.Numerics.Vector2 value ) => new Vector2( value ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static implicit operator System.Numerics.Vector2( Vector2 value ) => new System.Numerics.Vector2( value.x, value.y ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static implicit operator Vector2( double value ) => new Vector2( (float)value, (float)value ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static implicit operator Vector2( Vector3 value ) => new Vector2( value ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static implicit operator Vector2( Vector4 value ) => new Vector2( value ); #endregion #region equality [MethodImpl( MethodImplOptions.AggressiveInlining )] public static bool operator ==( Vector2 left, Vector2 right ) => left.Equals( right ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public static bool operator !=( Vector2 left, Vector2 right ) => !(left == right); public override readonly bool Equals( object obj ) => obj is Vector2 o && Equals( o ); [MethodImpl( MethodImplOptions.AggressiveInlining )] public readonly bool Equals( Vector2 o ) => (_vec) == (o._vec); public readonly override int GetHashCode() => _vec.GetHashCode(); #endregion /// /// Formats the vector into a string "x,y" /// public override readonly string ToString() { var _x = x; var _y = y; // avoid -0 if ( _x.AlmostEqual( 0 ) ) _x = 0.0f; if ( _y.AlmostEqual( 0 ) ) _y = 0.0f; return $"{_x:0.###},{_y:0.###}"; } /// /// Given a string, try to convert this into a Vector2. Example formatting is "x,y", "[x,y]", "x y", etc. /// public static Vector2 Parse( string str ) { if ( TryParse( str, CultureInfo.InvariantCulture, out var res ) ) return res; return default; } /// public static bool TryParse( string str, out Vector2 result ) { return TryParse( str, CultureInfo.InvariantCulture, out result ); } /// public static Vector2 Parse( string str, IFormatProvider provider ) { return Parse( str ); } /// public static bool TryParse( [NotNullWhen( true )] string str, IFormatProvider provider, [MaybeNullWhen( false )] out Vector2 result ) { result = Vector2.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 != 2 ) return false; if ( !float.TryParse( components[0], NumberStyles.Float, provider, out float x ) || !float.TryParse( components[1], NumberStyles.Float, provider, out float y ) ) { return false; } result = new Vector2( x, y ); return true; } /// /// Move to the target vector, by amount acceleration /// public readonly Vector2 WithAcceleration( Vector2 target, float accelerate ) { if ( target.IsNearZeroLength ) return this; Vector2 wishDir = target.Normal; float wishSpeed = target.Length; // See if we are changing direction a bit var currentSpeed = Dot( wishDir ); // Reduce wishspeed by the amount of veer var addSpeed = wishSpeed - currentSpeed; // If not going to add any speed, done. if ( addSpeed <= 0.0f ) return this; // Determine amount of acceleration var accelSpeed = accelerate * wishSpeed; // Cap at addSpeed if ( accelSpeed > addSpeed ) accelSpeed = addSpeed; return this + wishDir * accelSpeed; } public readonly Vector2 WithFriction( float frictionAmount, float stopSpeed = 140.0f ) { var speed = Length; if ( speed < 0.01f ) return this; // Bleed off some speed, but if we have less than the bleed // threshold, bleed the threshold amount float control = (speed < stopSpeed) ? stopSpeed : speed; // Add the amount to the drop amount var drop = control * frictionAmount; // Scale the velocity float newSpeed = speed - drop; if ( newSpeed < 0 ) newSpeed = 0; if ( newSpeed == speed ) return this; newSpeed /= speed; return this * newSpeed; } Vector2 IInterpolator.Interpolate( Vector2 a, Vector2 b, float delta ) { return a.LerpTo( b, delta ); } }