use crate::MathError;
use crate::aabb::Aabb3;
use crate::frame::Frame3;
use crate::nurbs::surface::NurbsSurface;
use crate::vec::{Point3, Vec3};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CylindricalSurface {
origin: Point3,
axis: Vec3,
radius: f64,
x_axis: Vec3,
y_axis: Vec3,
}
impl CylindricalSurface {
pub fn new(origin: Point3, axis: Vec3, radius: f64) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal(origin, axis)?;
Ok(Self {
origin,
axis: f.z,
radius,
x_axis: f.x,
y_axis: f.y,
})
}
#[must_use]
pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
let (sin_u, cos_u) = u.sin_cos();
self.origin
+ self.x_axis * (self.radius * cos_u)
+ self.y_axis * (self.radius * sin_u)
+ self.axis * v
}
#[must_use]
pub fn normal(&self, u: f64, _v: f64) -> Vec3 {
let (sin_u, cos_u) = u.sin_cos();
self.x_axis * cos_u + self.y_axis * sin_u
}
#[must_use]
pub const fn origin(&self) -> Point3 {
self.origin
}
#[must_use]
pub const fn axis(&self) -> Vec3 {
self.axis
}
#[must_use]
pub const fn radius(&self) -> f64 {
self.radius
}
#[must_use]
pub const fn x_axis(&self) -> Vec3 {
self.x_axis
}
#[must_use]
pub const fn y_axis(&self) -> Vec3 {
self.y_axis
}
pub fn with_ref_dir(
origin: Point3,
axis: Vec3,
radius: f64,
ref_dir: Vec3,
) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal_and_ref(origin, axis, ref_dir)?;
Ok(Self {
origin,
axis: f.z,
radius,
x_axis: f.x,
y_axis: f.y,
})
}
#[must_use]
pub fn translated(&self, offset: Vec3) -> Self {
Self {
origin: self.origin + offset,
..self.clone()
}
}
#[must_use]
pub fn project_point(&self, point: Point3) -> (f64, f64) {
let to_pt = Vec3::new(
point.x() - self.origin.x(),
point.y() - self.origin.y(),
point.z() - self.origin.z(),
);
let v = self.axis.dot(to_pt);
let radial = to_pt - self.axis * v;
let x = self.x_axis.dot(radial);
let y = self.y_axis.dot(radial);
let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
(u, v)
}
pub fn to_nurbs(&self, v_min: f64, v_max: f64) -> Result<NurbsSurface, MathError> {
let w1 = std::f64::consts::FRAC_1_SQRT_2;
let circle_weights = [1.0, w1, 1.0, w1, 1.0, w1, 1.0, w1, 1.0];
let dirs: [(f64, f64); 9] = [
(1.0, 0.0),
(1.0, 1.0),
(0.0, 1.0),
(-1.0, 1.0),
(-1.0, 0.0),
(-1.0, -1.0),
(0.0, -1.0),
(1.0, -1.0),
(1.0, 0.0),
];
let mut cps = Vec::with_capacity(9);
let mut ws = Vec::with_capacity(9);
for (i, &(dx, dy)) in dirs.iter().enumerate() {
let radial = self.x_axis * (self.radius * dx) + self.y_axis * (self.radius * dy);
let p_bot = self.origin + radial + self.axis * v_min;
let p_top = self.origin + radial + self.axis * v_max;
cps.push(vec![p_bot, p_top]);
ws.push(vec![circle_weights[i], circle_weights[i]]);
}
let knots_u = vec![
0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
];
let knots_v = vec![0.0, 0.0, 1.0, 1.0];
NurbsSurface::new(2, 1, knots_u, knots_v, cps, ws)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConicalSurface {
apex: Point3,
axis: Vec3,
half_angle: f64,
x_axis: Vec3,
y_axis: Vec3,
}
impl ConicalSurface {
pub fn new(apex: Point3, axis: Vec3, half_angle: f64) -> Result<Self, MathError> {
if half_angle <= 0.0 || half_angle >= std::f64::consts::FRAC_PI_2 {
return Err(MathError::ParameterOutOfRange {
value: half_angle,
min: f64::EPSILON,
max: std::f64::consts::FRAC_PI_2,
});
}
let f = Frame3::from_normal(apex, axis)?;
Ok(Self {
apex,
axis: f.z,
half_angle,
x_axis: f.x,
y_axis: f.y,
})
}
#[must_use]
pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_a, cos_a) = self.half_angle.sin_cos();
let radial = self.x_axis * cos_u + self.y_axis * sin_u;
self.apex + (radial * cos_a + self.axis * sin_a) * v
}
#[must_use]
pub fn normal(&self, u: f64, _v: f64) -> Vec3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_a, cos_a) = self.half_angle.sin_cos();
let radial = self.x_axis * cos_u + self.y_axis * sin_u;
radial * sin_a + self.axis * (-cos_a)
}
#[must_use]
pub const fn apex(&self) -> Point3 {
self.apex
}
#[must_use]
pub const fn axis(&self) -> Vec3 {
self.axis
}
#[must_use]
pub const fn half_angle(&self) -> f64 {
self.half_angle
}
#[must_use]
pub const fn x_axis(&self) -> Vec3 {
self.x_axis
}
#[must_use]
pub const fn y_axis(&self) -> Vec3 {
self.y_axis
}
pub fn with_ref_dir(
apex: Point3,
axis: Vec3,
half_angle: f64,
ref_dir: Vec3,
) -> Result<Self, MathError> {
if half_angle <= 0.0 || half_angle >= std::f64::consts::FRAC_PI_2 {
return Err(MathError::ParameterOutOfRange {
value: half_angle,
min: f64::EPSILON,
max: std::f64::consts::FRAC_PI_2,
});
}
let f = Frame3::from_normal_and_ref(apex, axis, ref_dir)?;
Ok(Self {
apex,
axis: f.z,
half_angle,
x_axis: f.x,
y_axis: f.y,
})
}
#[must_use]
pub fn translated(&self, offset: Vec3) -> Self {
Self {
apex: self.apex + offset,
..self.clone()
}
}
#[must_use]
pub fn radius_at(&self, v: f64) -> f64 {
v * self.half_angle.cos()
}
#[must_use]
pub fn project_point(&self, point: Point3) -> (f64, f64) {
let to_pt = Vec3::new(
point.x() - self.apex.x(),
point.y() - self.apex.y(),
point.z() - self.apex.z(),
);
let h = self.axis.dot(to_pt);
let radial = to_pt - self.axis * h;
let x = self.x_axis.dot(radial);
let y = self.y_axis.dot(radial);
let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
let sin_a = self.half_angle.sin();
let v = if sin_a.abs() > 1e-15 {
h / sin_a
} else {
let cos_a = self.half_angle.cos();
if cos_a.abs() > 1e-15 {
radial.length() / cos_a
} else {
0.0
}
};
(u, v)
}
pub fn to_nurbs(&self, v_min: f64, v_max: f64) -> Result<NurbsSurface, MathError> {
analytic_to_nurbs_sampled(
|u, v| self.evaluate(u, v),
(0.0, std::f64::consts::TAU),
(v_min, v_max),
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SphericalSurface {
center: Point3,
radius: f64,
x_axis: Vec3,
y_axis: Vec3,
z_axis: Vec3,
}
impl SphericalSurface {
pub fn new(center: Point3, radius: f64) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
Ok(Self {
center,
radius,
x_axis: Vec3::new(1.0, 0.0, 0.0),
y_axis: Vec3::new(0.0, 1.0, 0.0),
z_axis: Vec3::new(0.0, 0.0, 1.0),
})
}
pub fn with_axis(center: Point3, radius: f64, z_axis: Vec3) -> Result<Self, MathError> {
if radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal(center, z_axis)?;
Ok(Self {
center,
radius,
x_axis: f.x,
y_axis: f.y,
z_axis: f.z,
})
}
#[must_use]
pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_v, cos_v) = v.sin_cos();
self.center
+ self.x_axis * (self.radius * cos_v * cos_u)
+ self.y_axis * (self.radius * cos_v * sin_u)
+ self.z_axis * (self.radius * sin_v)
}
#[must_use]
pub fn normal(&self, u: f64, v: f64) -> Vec3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_v, cos_v) = v.sin_cos();
self.x_axis * (cos_v * cos_u) + self.y_axis * (cos_v * sin_u) + self.z_axis * sin_v
}
#[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 x_axis(&self) -> Vec3 {
self.x_axis
}
#[must_use]
pub const fn y_axis(&self) -> Vec3 {
self.y_axis
}
#[must_use]
pub const fn z_axis(&self) -> Vec3 {
self.z_axis
}
#[must_use]
pub fn translated(&self, offset: Vec3) -> Self {
Self {
center: self.center + offset,
..self.clone()
}
}
#[must_use]
pub fn aabb(&self) -> Aabb3 {
let r = self.radius;
Aabb3 {
min: Point3::new(
self.center.x() - r,
self.center.y() - r,
self.center.z() - r,
),
max: Point3::new(
self.center.x() + r,
self.center.y() + r,
self.center.z() + r,
),
}
}
#[must_use]
pub fn aabb_region(&self, pole: Vec3) -> Aabb3 {
let Ok(n) = pole.normalize() else {
return self.aabb();
};
let c = self.center;
let r = self.radius;
let span = |en: f64| -> (f64, f64) {
let s = (1.0 - en * en).max(0.0).sqrt();
let hi = if en >= 0.0 { 1.0 } else { s };
let lo = if en <= 0.0 { -1.0 } else { -s };
(lo, hi)
};
let (lx, hx) = span(n.x());
let (ly, hy) = span(n.y());
let (lz, hz) = span(n.z());
Aabb3 {
min: Point3::new(c.x() + r * lx, c.y() + r * ly, c.z() + r * lz),
max: Point3::new(c.x() + r * hx, c.y() + r * hy, c.z() + r * hz),
}
}
#[must_use]
pub fn project_point(&self, point: Point3) -> (f64, f64) {
let to_pt = Vec3::new(
point.x() - self.center.x(),
point.y() - self.center.y(),
point.z() - self.center.z(),
);
let r = to_pt.length();
if r < 1e-15 {
return (0.0, 0.0);
}
let x = self.x_axis.dot(to_pt);
let y = self.y_axis.dot(to_pt);
let z = self.z_axis.dot(to_pt);
let u = y.atan2(x).rem_euclid(std::f64::consts::TAU);
let v = (z / r).clamp(-1.0, 1.0).asin();
(u, v)
}
pub fn to_nurbs(&self) -> Result<NurbsSurface, MathError> {
analytic_to_nurbs_sampled(
|u, v| self.evaluate(u, v),
(0.0, std::f64::consts::TAU),
(-std::f64::consts::FRAC_PI_2, std::f64::consts::FRAC_PI_2),
)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ToroidalSurface {
center: Point3,
major_radius: f64,
minor_radius: f64,
x_axis: Vec3,
y_axis: Vec3,
z_axis: Vec3,
}
impl ToroidalSurface {
pub fn new(center: Point3, major_radius: f64, minor_radius: f64) -> Result<Self, MathError> {
if major_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: major_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
if minor_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: minor_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
Ok(Self {
center,
major_radius,
minor_radius,
x_axis: Vec3::new(1.0, 0.0, 0.0),
y_axis: Vec3::new(0.0, 1.0, 0.0),
z_axis: Vec3::new(0.0, 0.0, 1.0),
})
}
pub fn with_axis(
center: Point3,
major_radius: f64,
minor_radius: f64,
z_axis: Vec3,
) -> Result<Self, MathError> {
if major_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: major_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
if minor_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: minor_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal(center, z_axis)?;
Ok(Self {
center,
major_radius,
minor_radius,
x_axis: f.x,
y_axis: f.y,
z_axis: f.z,
})
}
pub fn with_axis_and_ref_dir(
center: Point3,
major_radius: f64,
minor_radius: f64,
z_axis: Vec3,
ref_dir: Vec3,
) -> Result<Self, MathError> {
if major_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: major_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
if minor_radius <= 0.0 {
return Err(MathError::ParameterOutOfRange {
value: minor_radius,
min: f64::EPSILON,
max: f64::MAX,
});
}
let f = Frame3::from_normal_and_ref(center, z_axis, ref_dir)?;
Ok(Self {
center,
major_radius,
minor_radius,
x_axis: f.x,
y_axis: f.y,
z_axis: f.z,
})
}
#[must_use]
pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_v, cos_v) = v.sin_cos();
let tube_radius = self.minor_radius.mul_add(cos_v, self.major_radius);
self.center
+ self.x_axis * (tube_radius * cos_u)
+ self.y_axis * (tube_radius * sin_u)
+ self.z_axis * (self.minor_radius * sin_v)
}
#[must_use]
pub fn normal(&self, u: f64, v: f64) -> Vec3 {
let (sin_u, cos_u) = u.sin_cos();
let (sin_v, cos_v) = v.sin_cos();
let radial = self.x_axis * cos_u + self.y_axis * sin_u;
radial * cos_v + self.z_axis * sin_v
}
#[must_use]
pub const fn center(&self) -> Point3 {
self.center
}
#[must_use]
pub fn translated(&self, offset: Vec3) -> Self {
Self {
center: self.center + offset,
..self.clone()
}
}
#[must_use]
pub fn aabb(&self) -> Aabb3 {
let rr = self.major_radius + self.minor_radius;
let r = self.minor_radius;
let hx = rr * self.x_axis.x().hypot(self.y_axis.x()) + r * self.z_axis.x().abs();
let hy = rr * self.x_axis.y().hypot(self.y_axis.y()) + r * self.z_axis.y().abs();
let hz = rr * self.x_axis.z().hypot(self.y_axis.z()) + r * self.z_axis.z().abs();
Aabb3 {
min: Point3::new(
self.center.x() - hx,
self.center.y() - hy,
self.center.z() - hz,
),
max: Point3::new(
self.center.x() + hx,
self.center.y() + hy,
self.center.z() + hz,
),
}
}
#[must_use]
pub const fn major_radius(&self) -> f64 {
self.major_radius
}
#[must_use]
pub const fn minor_radius(&self) -> f64 {
self.minor_radius
}
#[must_use]
pub const fn x_axis(&self) -> Vec3 {
self.x_axis
}
#[must_use]
pub const fn y_axis(&self) -> Vec3 {
self.y_axis
}
#[must_use]
pub const fn z_axis(&self) -> Vec3 {
self.z_axis
}
#[must_use]
pub fn project_point(&self, point: Point3) -> (f64, f64) {
let to_pt = Vec3::new(
point.x() - self.center.x(),
point.y() - self.center.y(),
point.z() - self.center.z(),
);
let x_comp = self.x_axis.dot(to_pt);
let y_comp = self.y_axis.dot(to_pt);
let u = y_comp.atan2(x_comp).rem_euclid(std::f64::consts::TAU);
let (sin_u, cos_u) = u.sin_cos();
let tube_center = self.center
+ self.x_axis * (self.major_radius * cos_u)
+ self.y_axis * (self.major_radius * sin_u);
let to_tube = Vec3::new(
point.x() - tube_center.x(),
point.y() - tube_center.y(),
point.z() - tube_center.z(),
);
let radial_dir = self.x_axis * cos_u + self.y_axis * sin_u;
let r_comp = radial_dir.dot(to_tube);
let z_comp = self.z_axis.dot(to_tube);
let v = z_comp.atan2(r_comp).rem_euclid(std::f64::consts::TAU);
(u, v)
}
pub fn to_nurbs(&self) -> Result<NurbsSurface, MathError> {
analytic_to_nurbs_sampled(
|u, v| self.evaluate(u, v),
(0.0, std::f64::consts::TAU),
(0.0, std::f64::consts::TAU),
)
}
}
#[derive(Debug, Clone)]
pub struct RevolutionSurface {
origin: Point3,
axis: Vec3,
x_axis: Vec3,
y_axis: Vec3,
generatrix_radii: Vec<f64>,
generatrix_heights: Vec<f64>,
}
impl RevolutionSurface {
pub fn new(
origin: Point3,
axis: Vec3,
radii: Vec<f64>,
heights: Vec<f64>,
) -> Result<Self, MathError> {
if radii.is_empty() || heights.is_empty() {
return Err(MathError::EmptyInput);
}
if radii.len() != heights.len() {
return Err(MathError::InvalidWeights {
expected: radii.len(),
got: heights.len(),
});
}
let f = Frame3::from_normal(origin, axis)?;
Ok(Self {
origin,
axis: f.z,
x_axis: f.x,
y_axis: f.y,
generatrix_radii: radii,
generatrix_heights: heights,
})
}
#[must_use]
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn evaluate(&self, u: f64, v: f64) -> Point3 {
let num_pts = self.generatrix_radii.len();
let param = v.clamp(0.0, 1.0) * (num_pts - 1) as f64;
let idx = (param as usize).min(num_pts - 2);
let frac = param - idx as f64;
let r = frac.mul_add(
self.generatrix_radii[idx + 1] - self.generatrix_radii[idx],
self.generatrix_radii[idx],
);
let height = frac.mul_add(
self.generatrix_heights[idx + 1] - self.generatrix_heights[idx],
self.generatrix_heights[idx],
);
let (sin_u, cos_u) = u.sin_cos();
self.origin + self.x_axis * (r * cos_u) + self.y_axis * (r * sin_u) + self.axis * height
}
#[must_use]
pub const fn origin(&self) -> Point3 {
self.origin
}
#[must_use]
pub const fn axis(&self) -> Vec3 {
self.axis
}
}
fn analytic_to_nurbs_sampled(
surface_fn: impl Fn(f64, f64) -> Point3,
u_range: (f64, f64),
v_range: (f64, f64),
) -> Result<NurbsSurface, MathError> {
let nu = 33;
let nv = 9;
let mut cps = Vec::with_capacity(nu);
let mut weights = Vec::with_capacity(nu);
#[allow(clippy::cast_precision_loss)]
for iu in 0..nu {
let u = u_range.0 + (u_range.1 - u_range.0) * (iu as f64 / (nu - 1) as f64);
let mut row = Vec::with_capacity(nv);
let mut w_row = Vec::with_capacity(nv);
for iv in 0..nv {
let v = v_range.0 + (v_range.1 - v_range.0) * (iv as f64 / (nv - 1) as f64);
row.push(surface_fn(u, v));
w_row.push(1.0);
}
cps.push(row);
weights.push(w_row);
}
let knots_u = uniform_clamped_knots(nu, 1);
let knots_v = uniform_clamped_knots(nv, 1);
NurbsSurface::new(1, 1, knots_u, knots_v, cps, weights)
}
#[allow(clippy::cast_precision_loss)]
fn uniform_clamped_knots(n: usize, degree: usize) -> Vec<f64> {
let mut k = vec![0.0; degree + 1];
for i in 1..n - degree {
k.push(i as f64 / (n - degree) as f64);
}
k.extend(vec![1.0; degree + 1]);
k
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests;