use super::Surface;
use glam::Vec3;
use rand::Rng;
use std::f32::consts::TAU;
pub struct TorusKnot {
pub p: i32,
pub q: i32,
pub big_r: f32,
pub r_knot: f32,
pub r_tube: f32,
}
impl TorusKnot {
pub fn new(p: i32, q: i32, big_r: f32, r_knot: f32, r_tube: f32) -> Self {
Self {
p: p.abs().max(2),
q: q.abs().max(1),
big_r: big_r.max(0.5),
r_knot: r_knot.max(0.1),
r_tube: r_tube.max(0.01),
}
}
fn curve(&self, t: f32) -> Vec3 {
let pt = self.p as f32 * t;
let qt = self.q as f32 * t;
let r = self.big_r + self.r_knot * qt.cos();
Vec3::new(r * pt.cos(), r * pt.sin(), self.r_knot * qt.sin())
}
fn curve_tangent(&self, t: f32) -> Vec3 {
const H: f32 = 1e-4;
(self.curve(t + H) - self.curve(t - H)) * (0.5 / H)
}
fn frame(&self, t: f32) -> (Vec3, Vec3) {
let tan = self.curve_tangent(t);
let tan_n = if tan.length() > 1e-12 {
tan.normalize()
} else {
Vec3::X
};
let up = if tan_n.dot(Vec3::Z).abs() < 0.9 {
Vec3::Z
} else {
Vec3::X
};
let binorm = tan_n.cross(up).normalize();
let normal = binorm.cross(tan_n).normalize();
(normal, binorm)
}
fn embed(&self, t: f32, s: f32) -> Vec3 {
let (norm, binorm) = self.frame(t);
self.curve(t) + self.r_tube * (s.cos() * norm + s.sin() * binorm)
}
fn dt(&self, t: f32, s: f32) -> Vec3 {
const H: f32 = 1e-4;
(self.embed(t + H, s) - self.embed(t - H, s)) * (0.5 / H)
}
fn ds(&self, t: f32, s: f32) -> Vec3 {
const H: f32 = 1e-4;
(self.embed(t, s + H) - self.embed(t, s - H)) * (0.5 / H)
}
}
impl Default for TorusKnot {
fn default() -> Self {
Self::new(2, 3, 2.0, 0.8, 0.15)
}
}
impl Surface for TorusKnot {
fn position(&self, u: f32, v: f32) -> Vec3 {
self.embed(u, v)
}
fn metric(&self, u: f32, v: f32) -> [[f32; 2]; 2] {
let eu = self.dt(u, v);
let ev = self.ds(u, v);
[[eu.dot(eu), eu.dot(ev)], [eu.dot(ev), ev.dot(ev)]]
}
fn christoffel(&self, u: f32, v: f32) -> [[[f32; 2]; 2]; 2] {
const H: f32 = 1e-3;
let g = self.metric(u, v);
let det = g[0][0] * g[1][1] - g[0][1] * g[0][1];
let inv_det = if det.abs() > 1e-12 { 1.0 / det } else { 0.0 };
let gi = [
[g[1][1] * inv_det, -g[0][1] * inv_det],
[-g[0][1] * inv_det, g[0][0] * inv_det],
];
let muh = self.metric(u + H, v);
let mul = self.metric(u - H, v);
let mvh = self.metric(u, v + H);
let mvl = self.metric(u, v - H);
let dg = [
[
[
(muh[0][0] - mul[0][0]) * 0.5 / H,
(mvh[0][0] - mvl[0][0]) * 0.5 / H,
],
[
(muh[0][1] - mul[0][1]) * 0.5 / H,
(mvh[0][1] - mvl[0][1]) * 0.5 / H,
],
],
[
[
(muh[1][0] - mul[1][0]) * 0.5 / H,
(mvh[1][0] - mvl[1][0]) * 0.5 / H,
],
[
(muh[1][1] - mul[1][1]) * 0.5 / H,
(mvh[1][1] - mvl[1][1]) * 0.5 / H,
],
],
];
let mut gamma = [[[0.0f32; 2]; 2]; 2];
for k in 0..2 {
for i in 0..2 {
for j in 0..2 {
let mut s = 0.0f32;
for l in 0..2 {
s += gi[k][l] * (dg[l][j][i] + dg[l][i][j] - dg[i][j][l]);
}
gamma[k][i][j] = 0.5 * s;
}
}
}
gamma
}
fn wrap(&self, u: f32, v: f32) -> (f32, f32) {
(u.rem_euclid(TAU), v.rem_euclid(TAU))
}
fn normal(&self, u: f32, v: f32) -> Vec3 {
let eu = self.dt(u, v);
let ev = self.ds(u, v);
let n = eu.cross(ev);
let len = n.length();
if len > 1e-12 {
n / len
} else {
Vec3::Z
}
}
fn random_position(&self, rng: &mut dyn rand::RngCore) -> (f32, f32) {
(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) {
let angle: f32 = rng.gen_range(0.0..TAU);
let du = angle.cos();
let dv = angle.sin();
let g = self.metric(u, v);
let speed_sq = g[0][0] * du * du + 2.0 * g[0][1] * du * dv + g[1][1] * dv * dv;
if speed_sq > 1e-12 {
let s = speed_sq.sqrt();
(du / s, dv / s)
} else {
(1.0, 0.0)
}
}
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)]
mod tests {
use super::*;
#[test]
fn position_is_finite() {
let tk = TorusKnot::default();
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 p = tk.position(u, v);
assert!(
p.x.is_finite() && p.y.is_finite() && p.z.is_finite(),
"position not finite at u={u:.3} v={v:.3}: {p:?}"
);
}
}
}
#[test]
fn metric_is_positive_definite() {
let tk = TorusKnot::default();
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 = tk.metric(u, v);
let det = g[0][0] * g[1][1] - g[0][1] * g[1][0];
assert!(
g[0][0] > 0.0,
"g_00 ≤ 0 at u={u:.3} v={v:.3}: g_00={}",
g[0][0]
);
assert!(det > 0.0, "det(g) ≤ 0 at u={u:.3} v={v:.3}: det={det}");
}
}
}
#[test]
fn christoffel_is_finite() {
let tk = TorusKnot::default();
let gamma = tk.christoffel(1.0, 0.5);
for k in 0..2 {
for i in 0..2 {
for j in 0..2 {
assert!(
gamma[k][i][j].is_finite(),
"Γ^{k}_{i}{j} not finite: {}",
gamma[k][i][j]
);
}
}
}
}
#[test]
fn christoffel_is_symmetric_in_lower_indices() {
let tk = TorusKnot::default();
let gamma = tk.christoffel(1.0, 1.0);
for k in 0..2 {
assert!(
(gamma[k][0][1] - gamma[k][1][0]).abs() < 1e-3,
"Γ^{k}_01 != Γ^{k}_10"
);
}
}
#[test]
fn trefoil_knot_is_periodic() {
let tk = TorusKnot::new(2, 3, 2.0, 0.8, 0.15);
let p0 = tk.curve(0.0);
let p1 = tk.curve(TAU);
assert!(
(p0 - p1).length() < 1e-3,
"Trefoil not periodic: {:?} vs {:?}",
p0,
p1
);
}
#[test]
fn normal_is_unit() {
let tk = TorusKnot::default();
let n = tk.normal(1.0, 1.0);
assert!(
(n.length() - 1.0).abs() < 1e-4,
"normal not unit: {}",
n.length()
);
}
#[test]
fn wrap_is_periodic() {
let tk = TorusKnot::default();
let (u, v) = tk.wrap(TAU + 0.5, -0.3);
assert!((0.0..TAU).contains(&u));
assert!((0.0..TAU).contains(&v));
}
#[test]
fn different_knot_types_give_different_shapes() {
let trefoil = TorusKnot::new(2, 3, 2.0, 0.8, 0.15);
let cinquefoil = TorusKnot::new(2, 5, 2.0, 0.8, 0.15);
let p_trefoil = trefoil.position(1.0, 1.0);
let p_cinquefoil = cinquefoil.position(1.0, 1.0);
assert!(
(p_trefoil - p_cinquefoil).length() > 0.01,
"Different knot types should give different positions"
);
}
}