use super::Surface;
use glam::Vec3;
use std::f32::consts::TAU;
pub struct Catenoid {
pub c: f32,
}
impl Catenoid {
pub fn new(c: f32) -> Self {
Self { c }
}
fn d_du(&self, u: f32, v: f32) -> Vec3 {
let lambda = self.c * (v / self.c).cosh();
Vec3::new(-lambda * u.sin(), lambda * u.cos(), 0.0)
}
fn d_dv(&self, u: f32, v: f32) -> Vec3 {
let sh = (v / self.c).sinh();
Vec3::new(sh * u.cos(), sh * u.sin(), 1.0)
}
}
impl Surface for Catenoid {
fn position(&self, u: f32, v: f32) -> Vec3 {
let lambda = self.c * (v / self.c).cosh();
Vec3::new(lambda * u.cos(), lambda * u.sin(), v)
}
fn metric(&self, u: f32, v: f32) -> [[f32; 2]; 2] {
let e1 = self.d_du(u, v);
let e2 = self.d_dv(u, v);
[[e1.dot(e1), e1.dot(e2)], [e1.dot(e2), e2.dot(e2)]]
}
fn christoffel(&self, _u: f32, v: f32) -> [[[f32; 2]; 2]; 2] {
let sigma_v = (v / self.c).tanh() / self.c;
[
[[0.0, sigma_v], [sigma_v, 0.0]],
[[-sigma_v, 0.0], [0.0, sigma_v]],
]
}
fn wrap(&self, u: f32, v: f32) -> (f32, f32) {
let u = u.rem_euclid(TAU);
let v_max = 3.0 * self.c;
let v = v.clamp(-v_max, v_max);
(u, v)
}
fn normal(&self, u: f32, v: f32) -> Vec3 {
let e1 = self.d_du(u, v);
let e2 = self.d_dv(u, v);
e1.cross(e2).normalize()
}
fn random_position(&self, rng: &mut dyn rand::RngCore) -> (f32, f32) {
use rand::Rng;
let v_max = 3.0 * self.c;
(rng.gen_range(0.0..TAU), rng.gen_range(-v_max..v_max))
}
fn random_tangent(&self, u: f32, v: f32, rng: &mut dyn rand::RngCore) -> (f32, f32) {
use rand::Rng;
let angle: f32 = rng.gen_range(0.0..TAU);
let g = self.metric(u, v);
let du = angle.cos() / g[0][0].sqrt().max(1e-8);
let dv = angle.sin() / g[1][1].sqrt().max(1e-8);
(du, dv)
}
fn mesh_vertices(&self, u_steps: u32, v_steps: u32) -> (Vec<[f32; 3]>, Vec<u32>) {
let v_max = 3.0 * self.c;
let mut verts = Vec::new();
let mut indices = Vec::new();
for i in 0..=u_steps {
for j in 0..=v_steps {
let u = (i as f32 / u_steps as f32) * TAU;
let v = -v_max + (j as f32 / v_steps as f32) * 2.0 * v_max;
let p = self.position(u, v);
verts.push([p.x, p.y, p.z]);
}
}
for i in 0..u_steps {
for j in 0..v_steps {
let a = i * (v_steps + 1) + j;
let b = a + 1;
let c = (i + 1) * (v_steps + 1) + j;
let d = c + 1;
indices.extend_from_slice(&[a, b, c, b, d, c]);
}
}
(verts, indices)
}
}
#[cfg(test)]
#[allow(clippy::needless_range_loop)]
mod tests {
use super::*;
#[test]
fn position_at_known_point() {
let cat = Catenoid::new(1.0);
let p = cat.position(0.0, 0.0);
assert!((p.x - 1.0).abs() < 1e-5, "x={}", p.x);
assert!(p.y.abs() < 1e-5, "y={}", p.y);
assert!(p.z.abs() < 1e-5, "z={}", p.z);
}
#[test]
fn metric_is_conformal() {
let cat = Catenoid::new(1.0);
let g = cat.metric(0.5, 0.3);
assert!((g[0][1]).abs() < 1e-5, "g_01 not zero: {}", g[0][1]);
assert!(
(g[0][0] - g[1][1]).abs() < 1e-4,
"g_00 != g_11: {} {}",
g[0][0],
g[1][1]
);
}
#[test]
fn christoffel_symmetry_ij() {
let cat = Catenoid::new(1.5);
let g = cat.christoffel(0.4, 0.8);
for k in 0..2 {
assert!(
(g[k][0][1] - g[k][1][0]).abs() < 1e-6,
"Γ^{k}_01 != Γ^{k}_10"
);
}
}
#[test]
fn wrap_clamps_v_and_wraps_u() {
let cat = Catenoid::new(1.0);
let (u, v) = cat.wrap(TAU + 0.5, 10.0);
assert!((0.0..TAU).contains(&u), "u={u}");
assert!(v <= 3.0, "v not clamped: {v}");
}
#[test]
fn normal_is_unit() {
let cat = Catenoid::new(1.0);
let n = cat.normal(1.0, 0.5);
assert!(
(n.length() - 1.0).abs() < 1e-5,
"normal length={}",
n.length()
);
}
#[test]
fn catenoid_metric_is_conformal() {
let c = Catenoid::new(1.0);
for v in [0.0f32, 0.5, 1.0, -0.7] {
let g = c.metric(0.3, v);
assert!(
(g[0][0] - g[1][1]).abs() < 1e-4,
"not conformal at v={v}: g00={} g11={}",
g[0][0],
g[1][1]
);
assert!(g[0][1].abs() < 1e-5, "g_uv != 0 at v={v}");
}
}
#[test]
fn catenoid_christoffel_symmetric() {
let c = Catenoid::new(1.5);
let g = c.christoffel(0.3, 0.7);
for k in 0..2 {
assert!(
(g[k][0][1] - g[k][1][0]).abs() < 1e-5,
"Γ^{k} not symmetric: {} vs {}",
g[k][0][1],
g[k][1][0]
);
}
}
#[test]
fn catenoid_position_at_waist() {
let c = Catenoid::new(1.5);
let p = c.position(0.0, 0.0);
assert!((p.x - 1.5).abs() < 1e-6, "waist x at u=0: {}", p.x);
assert!(p.y.abs() < 1e-6);
assert!(p.z.abs() < 1e-6);
}
}