// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Math { using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Helper; using ProtoBuf; /// /// Represents a parabola defined by y = A*x^2 + B*x, with x-intercepts at 0 and Length. /// The parabola opens downward with its vertex at (Length/2, MaxHeight). /// /// /// /// var p = new Parabola(maxHeight: 5f, length: 10f); /// p.TryGetValueAtNormalized(0.5f, out float peak); // peak == 5 /// /// [DataContract] [Serializable] [ProtoContract] public readonly struct Parabola : IEquatable { /// /// The distance between the two x-intercepts (at x=0 and x=Length). /// [DataMember] [ProtoMember(1)] public readonly float Length; /// /// The coefficient of x^2 in the parabola equation y = A*x^2 + B*x. /// [DataMember] [ProtoMember(2)] public readonly float A; /// /// The coefficient of x in the parabola equation y = A*x^2 + B*x. /// [DataMember] [ProtoMember(3)] public readonly float B; /// /// The maximum height of the parabola (y-value at the vertex). /// [DataMember] [ProtoMember(4)] public readonly float MaxHeight; /// /// The x-coordinate of the vertex (always at Length/2). /// public float VertexX => Length * 0.5f; /// /// The vertex position of the parabola. /// public (float x, float y) Vertex => (VertexX, MaxHeight); /// /// The valid x-range for this parabola [0, Length]. /// public (float min, float max) XRange => (0f, Length); /// /// Creates a Parabola that reaches a max height and has a specified length. /// /// Max height of parabola (must be greater than 0). /// Length of parabola between x intercepts (must be greater than 0). /// Thrown when maxHeight or length are not positive. [JsonConstructor] public Parabola(float maxHeight, float length) { if (length <= 0f) { throw new ArgumentException( $"Expected a length greater than 0, but found: {length:0.00}." ); } if (maxHeight <= 0f) { throw new ArgumentException( $"Expected a max height greater than 0, but found: {maxHeight:0.00}." ); } Length = length; MaxHeight = maxHeight; // For a parabola with intercepts at 0 and Length, and max height at Length/2: // y = A*x^2 + B*x // At x=0: y=0 (satisfied by having no constant term) // At x=Length: y=0, so A*Length^2 + B*Length = 0, thus B = -A*Length // At x=Length/2: y=maxHeight // Substituting: maxHeight = A*(Length/2)^2 + B*(Length/2) // maxHeight = A*Length^2/4 - A*Length^2/2 // maxHeight = -A*Length^2/4 // A = -4*maxHeight/Length^2 A = -4f * maxHeight / (length * length); B = -A * length; } internal Parabola(float maxHeight, float length, float a, float b) { Length = length; MaxHeight = maxHeight; A = a; B = b; } /// /// Creates a Parabola from explicit coefficients. /// /// Coefficient of x^2. /// Coefficient of x. /// Length of parabola (must be greater than 0). /// Thrown when parameters would create an invalid parabola. public static Parabola FromCoefficients(float a, float b, float length) { if (length <= 0f) { throw new ArgumentException( $"Expected a length greater than 0, but found: {length:0.00}." ); } if (a >= 0f) { throw new ArgumentException( $"Expected a negative coefficient A (downward parabola), but found: {a:0.00}." ); } // Verify that x=Length is an intercept: A*Length^2 + B*Length = 0 float valueAtLength = a * length * length + b * length; if (Math.Abs(valueAtLength) > 1e-5f) { throw new ArgumentException( $"Coefficients do not produce a parabola with intercept at x={length:0.00}. " + $"Value at x=Length is {valueAtLength:0.00}, expected ~0." ); } // Calculate max height from coefficients // Vertex x-coordinate: -B/(2A) // For our parabola with intercepts at 0 and Length, vertex should be at Length/2 float vertexX = -b / (2f * a); float maxHeight = a * vertexX * vertexX + b * vertexX; if (maxHeight <= 0f) { throw new ArgumentException( $"Calculated max height is not positive: {maxHeight:0.00}." ); } return new Parabola(maxHeight, length, a, b); } /// /// Evaluates the parabola at a given x-coordinate. /// /// The x-coordinate (must be in range [0, Length]). /// The resulting y-value, or NaN if x is out of range. /// True if x is within valid range, false otherwise. public bool TryGetValueAt(float x, out float y) { if (x < 0f || x > Length) { y = float.NaN; return false; } y = A * (x * x) + B * x; return true; } /// /// Evaluates the parabola at a normalized position. /// /// Normalized position along the parabola [0, 1]. /// The resulting y-value, or NaN if t is out of range. /// True if t is within valid range, false otherwise. public bool TryGetValueAtNormalized(float t, out float y) { if (t < 0f || t > 1f) { y = float.NaN; return false; } return TryGetValueAt(t * Length, out y); } /// /// Gets the value at a given x-coordinate without bounds checking. /// /// The x-coordinate. /// The y-value at x. public float GetValueAtUnchecked(float x) { return A * (x * x) + B * x; } public bool Equals(Parabola other) { return Length.Equals(other.Length) && A.Equals(other.A) && B.Equals(other.B) && MaxHeight.Equals(other.MaxHeight); } public override bool Equals(object obj) { return obj is Parabola other && Equals(other); } public override int GetHashCode() { return Objects.HashCode(Length, A, B, MaxHeight); } public static bool operator ==(Parabola left, Parabola right) { return left.Equals(right); } public static bool operator !=(Parabola left, Parabola right) { return !left.Equals(right); } public override string ToString() { return $"Parabola(maxHeight={MaxHeight:0.00}, length={Length:0.00}, vertex=({VertexX:0.00}, {MaxHeight:0.00}))"; } } }