// 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}))";
}
}
}