use super::Surface;
use glam::Vec3;
use std::f32::consts::TAU;
pub struct Torus {
pub big_r: f32,
pub small_r: f32,
}
impl Torus {
pub fn new(big_r: f32, small_r: f32) -> Self {
Self { big_r, small_r }
}
fn d_du(&self, u: f32, v: f32) -> Vec3 {
let r = self.big_r + self.small_r * v.cos();
Vec3::new(-r * u.sin(), r * u.cos(), 0.0)
}
fn d_dv(&self, u: f32, v: f32) -> Vec3 {
Vec3::new(
-self.small_r * v.sin() * u.cos(),
-self.small_r * v.sin() * u.sin(),
self.small_r * v.cos(),
)
}
#[allow(dead_code)]
fn d2_du2(&self, u: f32, v: f32) -> Vec3 {
let r = self.big_r + self.small_r * v.cos();
Vec3::new(-r * u.cos(), -r * u.sin(), 0.0)
}
#[allow(dead_code)]
fn d2_dv2(&self, u: f32, v: f32) -> Vec3 {
Vec3::new(
-self.small_r * v.cos() * u.cos(),
-self.small_r * v.cos() * u.sin(),
-self.small_r * v.sin(),
)
}
#[allow(dead_code)]
fn d2_dudv(&self, u: f32, v: f32) -> Vec3 {
Vec3::new(
self.small_r * v.sin() * u.sin(),
-self.small_r * v.sin() * u.cos(),
0.0,
)
}
fn metric_and_inv(&self, u: f32, v: f32) -> ([[f32; 2]; 2], [[f32; 2]; 2]) {
let e1 = self.d_du(u, v);
let e2 = self.d_dv(u, v);
let g00 = e1.dot(e1);
let g01 = e1.dot(e2);
let g11 = e2.dot(e2);
let g = [[g00, g01], [g01, g11]];
let det = g00 * g11 - g01 * g01;
let inv = [[g11 / det, -g01 / det], [-g01 / det, g00 / det]];
(g, inv)
}
}
impl Surface for Torus {
fn position(&self, u: f32, v: f32) -> Vec3 {
let r = self.big_r + self.small_r * v.cos();
Vec3::new(r * u.cos(), r * u.sin(), self.small_r * v.sin())
}
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 f = self.big_r + self.small_r * v.cos();
let df_dv = -self.small_r * v.sin();
let _g00 = f * f;
let g11 = self.small_r * self.small_r;
let gamma_0_01 = df_dv / f;
let gamma_1_00 = -f * df_dv / g11;
[
[[0.0, gamma_0_01], [gamma_0_01, 0.0]],
[[gamma_1_00, 0.0], [0.0, 0.0]],
]
}
fn wrap(&self, u: f32, v: f32) -> (f32, f32) {
let u = u.rem_euclid(TAU);
let v = v.rem_euclid(TAU);
(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;
(rng.gen_range(0.0..TAU), rng.gen_range(0.0..TAU))
}
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_and_inv(u, v);
let speed = 1.0;
let du = angle.cos() * speed / g[0][0].sqrt();
let dv = angle.sin() * speed / g[1][1].sqrt();
(du, dv)
}
fn mesh_vertices(&self, u_steps: u32, v_steps: u32) -> (Vec<[f32; 3]>, Vec<u32>) {
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 = (j as f32 / v_steps as f32) * TAU;
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::*;
use std::f32::consts::PI;
#[test]
fn position_at_known_point() {
let t = Torus::new(2.0, 0.7);
let p = t.position(0.0, 0.0);
assert!((p.x - 2.7).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_symmetric() {
let t = Torus::new(2.0, 0.7);
let g = t.metric(0.5, 1.2);
assert!((g[0][1] - g[1][0]).abs() < 1e-6);
}
#[test]
fn metric_is_positive_definite() {
let t = Torus::new(2.0, 0.7);
let g = t.metric(1.0, 0.5);
let det = g[0][0] * g[1][1] - g[0][1] * g[1][0];
assert!(g[0][0] > 0.0, "g_00 not positive: {}", g[0][0]);
assert!(det > 0.0, "det not positive: {det}");
}
#[test]
fn wrap_is_periodic() {
let t = Torus::new(2.0, 0.7);
let (u, v) = t.wrap(TAU + 0.3, -0.1);
assert!((0.0..TAU).contains(&u), "u={u}");
assert!((0.0..TAU).contains(&v), "v={v}");
}
#[test]
fn mesh_vertex_count() {
let t = Torus::new(2.0, 0.7);
let (verts, indices) = t.mesh_vertices(10, 10);
assert_eq!(verts.len(), 11 * 11);
assert_eq!(indices.len(), 10 * 10 * 6);
}
#[test]
fn degenerate_minor_radius_does_not_panic() {
let t = Torus::new(2.0, 0.0001);
let p = t.position(0.5, 0.5);
assert!(p.x.is_finite());
}
#[test]
fn christoffel_symmetry_ij() {
let t = Torus::new(2.0, 0.7);
let g = t.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 normal_is_unit() {
let t = Torus::new(2.0, 0.7);
let n = t.normal(1.0, 1.0);
assert!(
(n.length() - 1.0).abs() < 1e-5,
"normal length={}",
n.length()
);
}
#[test]
fn outer_equator_christoffel_all_zero() {
let t = Torus::new(2.0, 0.7);
let g = t.christoffel(0.0, 0.0);
for k in 0..2 {
for i in 0..2 {
for j in 0..2 {
assert!(
g[k][i][j].abs() < 1e-6,
"Γ^{k}_{i}{j} = {} at outer equator",
g[k][i][j]
);
}
}
}
}
#[test]
fn inner_equator_christoffel_nonzero() {
let t = Torus::new(2.0, 0.7);
let g = t.christoffel(0.0, PI / 2.0);
assert!(
g[0][0][1].abs() > 0.1,
"expected nonzero Γ^0_01: {}",
g[0][0][1]
);
}
#[test]
fn test_torus_metric_positive_definite() {
let t = Torus::new(2.0, 0.7);
for ui in 0..8u32 {
for vi in 0..8u32 {
let u = ui as f32 * TAU / 8.0;
let v = vi as f32 * TAU / 8.0;
let g = t.metric(u, v);
assert!(g[0][0] > 0.0, "g_00={} ≤ 0 at u={u} v={v}", g[0][0]);
assert!(g[1][1] > 0.0, "g_11={} ≤ 0 at u={u} v={v}", g[1][1]);
let det = g[0][0] * g[1][1] - g[0][1] * g[0][1];
assert!(det > 0.0, "det(g)={det} ≤ 0 at u={u} v={v}");
}
}
}
#[test]
fn test_torus_geodesic_great_circle() {
use crate::geodesic::Geodesic;
let t = Torus::new(2.0, 0.7);
let g = t.metric(0.0, 0.0);
let du = 1.0 / g[0][0].sqrt();
let mut geo = Geodesic::new(0.0, 0.0, du, 0.0, 1000, 0);
for _ in 0..200 {
geo.step(&t, 0.016);
}
let v_dist = geo.v.min(TAU - geo.v);
assert!(
v_dist < 0.05,
"v drifted from equator: wrapped v = {}",
geo.v
);
}
#[test]
fn test_christoffel_symbols_count() {
let t = Torus::new(2.0, 0.7);
let gamma = t.christoffel(0.5, 0.5);
let mut count = 0usize;
#[allow(clippy::needless_range_loop)]
for k in 0..2usize {
for i in 0..2usize {
for j in 0..2usize {
let _ = gamma[k][i][j];
count += 1;
}
}
}
assert_eq!(count, 8, "expected 8 Christoffel components, got {count}");
}
#[test]
fn test_torus_normal_unit_length() {
let t = Torus::new(2.0, 0.7);
for ui in 0..8u32 {
for vi in 0..8u32 {
let u = ui as f32 * TAU / 8.0;
let v = vi as f32 * TAU / 8.0;
let n = t.normal(u, v);
assert!(
(n.length() - 1.0).abs() < 1e-5,
"normal not unit at u={u:.3} v={v:.3}: |n|={}",
n.length()
);
}
}
}
#[test]
fn test_torus_christoffel_symmetry_grid() {
let t = Torus::new(2.0, 0.7);
for ui in 0..6u32 {
for vi in 0..6u32 {
let u = ui as f32 * TAU / 6.0;
let v = vi as f32 * TAU / 6.0;
let g = t.christoffel(u, v);
for k in 0..2 {
assert!(
(g[k][0][1] - g[k][1][0]).abs() < 1e-6,
"Γ^{k}_01 != Γ^{k}_10 at u={u:.3} v={v:.3}"
);
}
}
}
}
}