use core::cmp::Ordering;
mod line;
mod segment;
pub use line::Line;
pub use segment::Segment;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Parabola {
pub a: f64,
pub b: f64,
pub c: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Roots {
NoRoots,
One(f64),
Two(f64, f64),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Parabola {
#[inline]
#[must_use]
pub fn from_square(a: f64, h: f64, k: f64) -> Self {
Self {
a,
b: 2.0 * a * h,
c: a * h.powi(2) + k,
}
}
#[inline]
#[must_use]
pub fn square_coefs(&self) -> (f64, f64, f64) {
let h = self.b / (2.0 * self.a);
let k = self.c - self.a * h.powi(2);
(self.a, h, k)
}
}
impl Parabola {
#[inline]
#[must_use]
pub fn eval(&self, x: f64) -> f64 {
x * (self.a * x + self.b) + self.c
}
#[inline]
#[must_use]
pub fn eval_deriv(&self, x: f64) -> f64 {
2.0 * self.a * x + self.b
}
#[inline]
#[must_use]
#[doc(alias = "eval_deriv2")]
pub fn deriv2(&self) -> f64 {
2.0 * self.a
}
#[must_use]
#[doc(alias = "x_intercepts")]
pub fn roots(&self) -> Roots {
let (a, b, c) = (self.a, self.b, self.c);
if a == 0.0 {
if b == 0.0 {
return Roots::NoRoots;
}
return Roots::One(-c / b);
}
let disc = b.powi(2) - 4.0 * a * c;
match disc.total_cmp(&0.0) {
Ordering::Less => Roots::NoRoots,
Ordering::Greater => {
if b == 0.0 {
debug_assert!((-c / a).is_sign_positive());
let root = (-c / a).sqrt();
Roots::Two(-root, root)
} else {
let signb = b.signum();
let temp = -b.midpoint(signb * disc.sqrt());
let (root1, root2) = (temp / a, c / temp);
if root1 < root2 {
Roots::Two(root1, root2)
} else {
Roots::Two(root2, root1)
}
}
}
Ordering::Equal => Roots::One((-b) / (2.0 * a)),
}
}
#[inline]
#[must_use]
pub fn axis(&self) -> Option<f64> {
if self.a == 0.0 {
None
} else {
Some(-self.b / (2.0 * self.a))
}
}
#[inline]
#[must_use]
pub fn y_intercept(&self) -> f64 {
self.c
}
#[inline]
#[must_use]
pub fn minimum(&self) -> Option<f64> {
if self.a > 0.0 {
self.axis().map(|axis| self.eval(axis))
} else {
None
}
}
#[inline]
#[must_use]
pub fn maximum(&self) -> Option<f64> {
if self.a < 0.0 {
self.axis().map(|axis| self.eval(axis))
} else {
None
}
}
#[inline]
#[must_use]
pub fn vertex(&self) -> Option<Point> {
self.axis().map(|axis| Point {
x: axis,
y: {
let (a, b, c) = (self.a, self.b, self.c);
(4.0 * a * c - b.powi(2)) / (4.0 * a)
},
})
}
#[inline]
#[must_use]
pub fn focus(&self) -> Option<Point> {
self.axis().map(|axis| Point {
x: axis,
y: {
let (a, b, c) = (self.a, self.b, self.c);
(4.0 * a * c - b.powi(2) + 1.0) / (4.0 * a)
},
})
}
#[inline]
#[must_use]
pub fn directix(&self) -> Option<Line> {
if self.a == 0.0 {
None
} else {
let (a, b, c) = (self.a, self.b, self.c);
Some(Line {
slope: 0.0,
intercept: (4.0 * a * c - b.powi(2) - 1.0) / (4.0 * a),
})
}
}
#[inline]
#[must_use]
pub fn focal_length(&self) -> Option<f64> {
if self.a == 0.0 {
None
} else {
Some((1.0 / (4.0 * self.a)).abs())
}
}
#[inline]
#[must_use]
pub fn latus_rectum(&self) -> Option<Segment> {
self.focus().map(|focus| {
#[expect(clippy::missing_panics_doc, reason = "'a' is nonzero here.")]
let length = 4.0 * self.focal_length().expect("'a' is nonzero here.");
let start = Point {
x: focus.x - length / 2.0,
y: focus.y,
};
let end = Point {
x: focus.x + length / 2.0,
y: focus.y,
};
Segment { start, end }
})
}
#[inline]
#[must_use]
pub fn project(&self, point: Point) -> Point {
Point {
x: point.x,
y: self.eval(point.x),
}
}
#[inline]
#[must_use]
pub fn contains(&self, point: Point) -> bool {
match self.a.total_cmp(&0.0) {
Ordering::Equal => panic!("Zero 'a' coefficient encountered"),
Ordering::Less => {
let proj = self.project(point);
point.y <= proj.y
}
Ordering::Greater => {
let proj = self.project(point);
point.y >= proj.y
}
}
}
#[inline]
#[must_use]
pub fn tangent(&self, x: f64) -> Line {
let point = Point { x, y: self.eval(x) };
Line::from_slope_point(self.eval_deriv(x), point)
}
}