use std::f64::consts::PI;
use crate::MathError;
use crate::aabb::Aabb3;
use crate::frame::Frame3;
use crate::vec::{Point3, Vec3};
fn conic_aabb(center: Point3, (a, u): (f64, Vec3), (b, v): (f64, Vec3)) -> Aabb3 {
let reach = |ui: f64, vi: f64| (a * ui).hypot(b * vi);
let r = Vec3::new(
reach(u.x(), v.x()),
reach(u.y(), v.y()),
reach(u.z(), v.z()),
);
Aabb3 {
min: center - r,
max: center + r,
}
}
fn conic_arc_aabb(
center: Point3,
(a, u): (f64, Vec3),
(b, v): (f64, Vec3),
(t0, t1): (f64, f64),
) -> Aabb3 {
use std::f64::consts::TAU;
if (t1 - t0).abs() >= TAU {
return conic_aabb(center, (a, u), (b, v));
}
let (lo, hi) = if t1 >= t0 { (t0, t1) } else { (t1, t0) };
let at = |t: f64| center + u * (a * t.cos()) + v * (b * t.sin());
let mut pts = vec![at(lo), at(hi)];
for (ui, vi) in [(u.x(), v.x()), (u.y(), v.y()), (u.z(), v.z())] {
let peak = (b * vi).atan2(a * ui);
for t in [peak, peak + PI] {
let t = lo + (t - lo).rem_euclid(TAU);
if t <= hi {
pts.push(at(t));
}
}
}
Aabb3::from_points(pts)
}
#[derive(Debug, Clone)]
pub struct Line3D {
origin: Point3,
direction: Vec3,
}
impl Line3D {
pub fn new(origin: Point3, direction: Vec3) -> Result<Self, MathError> {
let len = direction.length();
if len < 1e-15 {
return Err(MathError::ZeroVector);
}
Ok(Self {
origin,
direction: Vec3::new(
direction.x() / len,
direction.y() / len,
direction.z() / len,
),
})
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point3 {
self.origin + self.direction * t
}
#[must_use]
pub const fn tangent(&self) -> Vec3 {
self.direction
}
#[must_use]
pub fn project(&self, point: Point3) -> f64 {
let v = point - self.origin;
self.direction.dot(v)
}
#[must_use]
pub fn distance_to_point(&self, point: Point3) -> f64 {
let v = point - self.origin;
let proj = self.direction * self.direction.dot(v);
(v - proj).length()
}
#[must_use]
pub const fn origin(&self) -> Point3 {
self.origin
}
#[must_use]
pub const fn direction(&self) -> Vec3 {
self.direction
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Circle3D {
center: Point3,
normal: Vec3,
radius: f64,
u_axis: Vec3,
v_axis: Vec3,
}
impl Circle3D {
pub fn new(center: Point3, normal: Vec3, radius: f64) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: 0.0,
max: f64::INFINITY,
});
}
let f = Frame3::from_normal(center, normal)?;
Ok(Self {
center,
normal: f.z,
radius,
u_axis: f.x,
v_axis: f.y,
})
}
pub fn new_with_ref(
center: Point3,
normal: Vec3,
radius: f64,
ref_dir: Vec3,
) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: 0.0,
max: f64::INFINITY,
});
}
let f = Frame3::from_normal_and_ref(center, normal, ref_dir)?;
Ok(Self {
center,
normal: f.z,
radius,
u_axis: f.x,
v_axis: f.y,
})
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point3 {
let cos_t = t.cos();
let sin_t = t.sin();
self.center + self.u_axis * (self.radius * cos_t) + self.v_axis * (self.radius * sin_t)
}
#[must_use]
pub fn tangent(&self, t: f64) -> Vec3 {
let cos_t = t.cos();
let sin_t = t.sin();
self.u_axis * (-sin_t) + self.v_axis * cos_t
}
#[must_use]
pub fn circumference(&self) -> f64 {
2.0 * PI * self.radius
}
#[must_use]
pub const fn center(&self) -> Point3 {
self.center
}
#[must_use]
pub const fn radius(&self) -> f64 {
self.radius
}
#[must_use]
pub const fn normal(&self) -> Vec3 {
self.normal
}
#[must_use]
pub fn project(&self, point: Point3) -> f64 {
let v = point - self.center;
let u_comp = self.u_axis.dot(v);
let v_comp = self.v_axis.dot(v);
v_comp.atan2(u_comp)
}
#[must_use]
pub const fn u_axis(&self) -> Vec3 {
self.u_axis
}
#[must_use]
pub const fn v_axis(&self) -> Vec3 {
self.v_axis
}
#[must_use]
pub fn aabb(&self) -> Aabb3 {
conic_aabb(
self.center,
(self.radius, self.u_axis),
(self.radius, self.v_axis),
)
}
#[must_use]
pub fn arc_aabb(&self, t0: f64, t1: f64) -> Aabb3 {
conic_arc_aabb(
self.center,
(self.radius, self.u_axis),
(self.radius, self.v_axis),
(t0, t1),
)
}
pub fn with_axes(
center: Point3,
normal: Vec3,
radius: f64,
u_axis: Vec3,
v_axis: Vec3,
) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: 0.0,
max: f64::INFINITY,
});
}
Ok(Self {
center,
normal,
radius,
u_axis,
v_axis,
})
}
#[must_use]
pub fn intersect_segment(
&self,
seg_start: Point3,
seg_end: Point3,
tol: f64,
) -> Vec<(Point3, f64)> {
let mut out = Vec::new();
let d = seg_end - seg_start;
let seg_len_sq = d.length_squared();
if seg_len_sq < tol * tol {
return out;
}
let h0 = (seg_start - self.center).dot(self.normal);
let h1 = (seg_end - self.center).dot(self.normal);
let on_plane = |p: Point3| -> bool {
let v = p - self.center;
let in_plane = v.dot(self.normal).abs() < tol;
let r = v.length();
in_plane && (r - self.radius).abs() < tol
};
let mut push_if_unique = |p: Point3| {
let v = p - self.center;
let mut t = v.dot(self.v_axis).atan2(v.dot(self.u_axis));
if t < 0.0 {
t += std::f64::consts::TAU;
}
if out
.iter()
.any(|(q, _): &(Point3, f64)| (*q - p).length() < tol)
{
return;
}
out.push((p, t));
};
if h0.abs() < tol && h1.abs() < tol {
let p0_u = (seg_start - self.center).dot(self.u_axis);
let p0_v = (seg_start - self.center).dot(self.v_axis);
let p1_u = (seg_end - self.center).dot(self.u_axis);
let p1_v = (seg_end - self.center).dot(self.v_axis);
let du = p1_u - p0_u;
let dv = p1_v - p0_v;
let a = du * du + dv * dv;
let b = p0_u * du + p0_v * dv;
let c = p0_u * p0_u + p0_v * p0_v - self.radius * self.radius;
let disc = b * b - a * c;
if a < tol * tol || disc < -tol * tol * a {
return out;
}
let disc = disc.max(0.0);
let s_slack = tol / seg_len_sq.sqrt();
let sqrt_disc = disc.sqrt();
let roots: &[f64] = if disc <= 2.0 * self.radius * tol * a {
&[-b / a]
} else {
&[(-b - sqrt_disc) / a, (-b + sqrt_disc) / a]
};
for &s in roots {
if s >= -s_slack && s <= 1.0 + s_slack {
let s = s.clamp(0.0, 1.0);
let p = Point3::new(
seg_start.x() + s * d.x(),
seg_start.y() + s * d.y(),
seg_start.z() + s * d.z(),
);
push_if_unique(p);
}
}
} else if h0 * h1 <= tol * tol {
let denom = h0 - h1;
if denom.abs() < tol {
return out;
}
let s = h0 / denom;
let s_slack = tol / seg_len_sq.sqrt();
if s < -s_slack || s > 1.0 + s_slack {
return out;
}
let s = s.clamp(0.0, 1.0);
let p = Point3::new(
seg_start.x() + s * d.x(),
seg_start.y() + s * d.y(),
seg_start.z() + s * d.z(),
);
if on_plane(p) {
push_if_unique(p);
}
}
out
}
#[must_use]
pub fn intersect_circle(&self, other: &Self, tol: f64) -> Vec<(Point3, f64)> {
let mut out = Vec::new();
if self.normal.cross(other.normal).length() > 1e-9 {
return self.intersect_skew_circle(other, tol);
}
let dvec = other.center - self.center;
if dvec.dot(self.normal).abs() > tol {
return out; }
let du = dvec.dot(self.u_axis);
let dv = dvec.dot(self.v_axis);
let d2 = du * du + dv * dv;
let d = d2.sqrt();
if d < tol {
return out; }
let (r1, r2) = (self.radius, other.radius);
let a = (d2 + r1 * r1 - r2 * r2) / (2.0 * d);
let h2 = r1 * r1 - a * a;
let r_eff = r1.min(r2);
if h2 < -2.0 * r_eff * tol {
return out; }
let ux = Vec3::new(
(self.u_axis.x() * du + self.v_axis.x() * dv) / d,
(self.u_axis.y() * du + self.v_axis.y() * dv) / d,
(self.u_axis.z() * du + self.v_axis.z() * dv) / d,
);
let vx = self.normal.cross(ux);
let foot = self.center + ux * a;
let mut push = |p: Point3| {
let v = p - self.center;
let mut t = v.dot(self.v_axis).atan2(v.dot(self.u_axis));
if t < 0.0 {
t += std::f64::consts::TAU;
}
if !out
.iter()
.any(|(q, _): &(Point3, f64)| (*q - p).length() < tol)
{
out.push((p, t));
}
};
if h2 <= 2.0 * r_eff * tol {
push(foot);
} else {
let h = h2.sqrt();
push(foot + vx * h);
push(foot - vx * h);
}
out
}
fn intersect_skew_circle(&self, other: &Self, tol: f64) -> Vec<(Point3, f64)> {
let mut out: Vec<(Point3, f64)> = Vec::new();
let (n1, n2) = (self.normal, other.normal);
let Ok(dir) = n1.cross(n2).normalize() else {
return out;
};
let (h1, h2) = (
n1.dot(Vec3::new(self.center.x(), self.center.y(), self.center.z())),
n2.dot(Vec3::new(
other.center.x(),
other.center.y(),
other.center.z(),
)),
);
let (a, b, c) = (n1.dot(n1), n2.dot(n2), n1.dot(n2));
let det = a.mul_add(b, -(c * c));
let base = n1 * ((h1 * b - h2 * c) / det) + n2 * ((h2 * a - h1 * c) / det);
let base = Point3::new(base.x(), base.y(), base.z());
let off = base - self.center;
let half_b = dir.dot(off);
let disc = half_b.mul_add(half_b, -(off.dot(off) - self.radius * self.radius));
let well = 2.0 * self.radius * tol;
if disc < -well {
return out;
}
let roots: Vec<f64> = if disc <= well {
vec![-half_b]
} else {
let root = disc.sqrt();
vec![-half_b - root, -half_b + root]
};
for s in roots {
let p = base + dir * s;
if ((p - other.center).length() - other.radius).abs() > tol {
continue;
}
let v = p - self.center;
let mut t = v.dot(self.v_axis).atan2(v.dot(self.u_axis));
if t < 0.0 {
t += std::f64::consts::TAU;
}
if !out.iter().any(|(q, _)| (*q - p).length() < tol) {
out.push((p, t));
}
}
out
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ellipse3D {
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
u_axis: Vec3,
v_axis: Vec3,
}
impl Ellipse3D {
pub fn new(
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
) -> Result<Self, MathError> {
if semi_major <= 0.0 || semi_minor <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: semi_major.min(semi_minor),
min: 0.0,
max: f64::INFINITY,
});
}
if semi_minor > semi_major {
return Err(MathError::ParameterOutOfRange {
value: semi_minor,
min: 0.0,
max: semi_major,
});
}
let f = Frame3::from_normal(center, normal)?;
Ok(Self {
center,
normal: f.z,
semi_major,
semi_minor,
u_axis: f.x,
v_axis: f.y,
})
}
pub fn new_with_ref(
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
ref_dir: Vec3,
) -> Result<Self, MathError> {
if semi_major <= 0.0 || semi_minor <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: semi_major.min(semi_minor),
min: 0.0,
max: f64::INFINITY,
});
}
if semi_minor > semi_major {
return Err(MathError::ParameterOutOfRange {
value: semi_minor,
min: 0.0,
max: semi_major,
});
}
let f = Frame3::from_normal_and_ref(center, normal, ref_dir)?;
Ok(Self {
center,
normal: f.z,
semi_major,
semi_minor,
u_axis: f.x,
v_axis: f.y,
})
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point3 {
let cos_t = t.cos();
let sin_t = t.sin();
self.center
+ self.u_axis * (self.semi_major * cos_t)
+ self.v_axis * (self.semi_minor * sin_t)
}
#[must_use]
pub fn tangent(&self, t: f64) -> Vec3 {
let cos_t = t.cos();
let sin_t = t.sin();
self.u_axis * (-self.semi_major * sin_t) + self.v_axis * (self.semi_minor * cos_t)
}
#[must_use]
pub const fn center(&self) -> Point3 {
self.center
}
#[must_use]
pub const fn semi_major(&self) -> f64 {
self.semi_major
}
#[must_use]
pub const fn semi_minor(&self) -> f64 {
self.semi_minor
}
#[must_use]
pub const fn normal(&self) -> Vec3 {
self.normal
}
#[must_use]
pub fn approximate_circumference(&self) -> f64 {
let a = self.semi_major;
let b = self.semi_minor;
let h = (a - b) * (a - b) / ((a + b) * (a + b));
PI * (a + b) * (1.0 + 3.0 * h / (10.0 + (3.0f64.mul_add(-h, 4.0)).sqrt()))
}
#[must_use]
pub fn project(&self, point: Point3) -> f64 {
let v = point - self.center;
let u_comp = self.u_axis.dot(v) / self.semi_major;
let v_comp = self.v_axis.dot(v) / self.semi_minor;
v_comp.atan2(u_comp)
}
#[must_use]
pub const fn u_axis(&self) -> Vec3 {
self.u_axis
}
#[must_use]
pub const fn v_axis(&self) -> Vec3 {
self.v_axis
}
#[must_use]
pub fn aabb(&self) -> Aabb3 {
conic_aabb(
self.center,
(self.semi_major, self.u_axis),
(self.semi_minor, self.v_axis),
)
}
#[must_use]
pub fn arc_aabb(&self, t0: f64, t1: f64) -> Aabb3 {
conic_arc_aabb(
self.center,
(self.semi_major, self.u_axis),
(self.semi_minor, self.v_axis),
(t0, t1),
)
}
pub fn with_axes(
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
u_axis: Vec3,
v_axis: Vec3,
) -> Result<Self, MathError> {
if semi_major <= 0.0 || semi_minor <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: semi_major.min(semi_minor),
min: 0.0,
max: f64::INFINITY,
});
}
Ok(Self {
center,
normal,
semi_major,
semi_minor,
u_axis,
v_axis,
})
}
}
#[derive(Debug, Clone)]
pub struct Parabola3D {
vertex: Point3,
axis_dir: Vec3,
focal_length: f64,
u_axis: Vec3,
}
impl Parabola3D {
pub fn new(vertex: Point3, axis_dir: Vec3, focal_length: f64) -> Result<Self, MathError> {
if focal_length <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: focal_length,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal(vertex, axis_dir)?;
Ok(Self {
vertex,
axis_dir: f.z,
focal_length,
u_axis: f.x,
})
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point3 {
let along_axis = (t * t) / (4.0 * self.focal_length);
self.vertex + self.axis_dir * along_axis + self.u_axis * t
}
#[must_use]
pub fn tangent(&self, t: f64) -> Vec3 {
let d_axis = t / (2.0 * self.focal_length);
self.axis_dir * d_axis + self.u_axis
}
#[must_use]
pub fn curvature(&self, t: f64) -> f64 {
let two_f = 2.0 * self.focal_length;
let ratio = t / two_f;
let denom = ratio.mul_add(ratio, 1.0);
1.0 / (two_f * denom.powf(1.5))
}
#[must_use]
pub const fn vertex(&self) -> Point3 {
self.vertex
}
#[must_use]
pub const fn focal_length(&self) -> f64 {
self.focal_length
}
#[must_use]
pub const fn axis_dir(&self) -> Vec3 {
self.axis_dir
}
#[must_use]
pub const fn u_axis(&self) -> Vec3 {
self.u_axis
}
#[must_use]
pub fn focus(&self) -> Point3 {
self.vertex + self.axis_dir * self.focal_length
}
}
#[derive(Debug, Clone)]
pub struct Hyperbola3D {
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
u_axis: Vec3,
v_axis: Vec3,
}
impl Hyperbola3D {
pub fn new(
center: Point3,
normal: Vec3,
semi_major: f64,
semi_minor: f64,
) -> Result<Self, MathError> {
if semi_major <= 0.0 || semi_minor <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: semi_major.min(semi_minor),
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal(center, normal)?;
Ok(Self {
center,
normal: f.z,
semi_major,
semi_minor,
u_axis: f.x,
v_axis: f.y,
})
}
#[must_use]
pub fn evaluate(&self, t: f64) -> Point3 {
self.center
+ self.u_axis * (self.semi_major * t.cosh())
+ self.v_axis * (self.semi_minor * t.sinh())
}
#[must_use]
pub fn tangent(&self, t: f64) -> Vec3 {
self.u_axis * (self.semi_major * t.sinh()) + self.v_axis * (self.semi_minor * t.cosh())
}
#[must_use]
pub const fn center(&self) -> Point3 {
self.center
}
#[must_use]
pub const fn semi_major(&self) -> f64 {
self.semi_major
}
#[must_use]
pub const fn semi_minor(&self) -> f64 {
self.semi_minor
}
#[must_use]
pub const fn normal(&self) -> Vec3 {
self.normal
}
#[must_use]
pub const fn u_axis(&self) -> Vec3 {
self.u_axis
}
#[must_use]
pub const fn v_axis(&self) -> Vec3 {
self.v_axis
}
#[must_use]
pub fn eccentricity(&self) -> f64 {
let ratio = self.semi_minor / self.semi_major;
ratio.mul_add(ratio, 1.0).sqrt()
}
#[must_use]
pub fn foci(&self) -> (Point3, Point3) {
let c = self.semi_major.hypot(self.semi_minor);
(
self.center + self.u_axis * c,
self.center + self.u_axis * (-c),
)
}
}
#[cfg(test)]
mod tests;