Skip to main content

proof_engine/geometry/
curvature.rs

1//! Curvature visualization — Gaussian, mean curvature rendered as glyph color.
2
3use glam::{Vec3, Vec4};
4use super::GeoMesh;
5use std::collections::HashMap;
6
7/// Type of curvature to compute.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CurvatureType {
10    Gaussian,
11    Mean,
12    Principal1,
13    Principal2,
14}
15
16/// Per-vertex curvature field for a mesh.
17#[derive(Debug, Clone)]
18pub struct CurvatureField {
19    pub curvature_type: CurvatureType,
20    pub values: Vec<f32>,
21    pub min_value: f32,
22    pub max_value: f32,
23}
24
25impl CurvatureField {
26    /// Compute curvature for each vertex of the mesh.
27    pub fn compute(mesh: &GeoMesh, curvature_type: CurvatureType) -> Self {
28        let n = mesh.vertices.len();
29        let mut values = vec![0.0f32; n];
30
31        // Build vertex adjacency + one-ring neighborhoods
32        let adj = build_one_ring(mesh);
33
34        for vi in 0..n {
35            let p = mesh.vertices[vi];
36            let neighbors = match adj.get(&(vi as u32)) {
37                Some(n) => n,
38                None => continue,
39            };
40            if neighbors.is_empty() { continue; }
41
42            match curvature_type {
43                CurvatureType::Gaussian => {
44                    // Angle deficit method: K = (2π - Σθ) / A
45                    let mut angle_sum = 0.0f32;
46                    let mut area = 0.0f32;
47
48                    for i in 0..neighbors.len() {
49                        let j = (i + 1) % neighbors.len();
50                        let a = mesh.vertices[neighbors[i] as usize] - p;
51                        let b = mesh.vertices[neighbors[j] as usize] - p;
52                        let cos_angle = a.dot(b) / (a.length() * b.length()).max(1e-10);
53                        angle_sum += cos_angle.clamp(-1.0, 1.0).acos();
54                        area += a.cross(b).length() * 0.5;
55                    }
56
57                    let mixed_area = (area / 3.0).max(1e-10);
58                    values[vi] = (std::f32::consts::TAU - angle_sum) / mixed_area;
59                }
60                CurvatureType::Mean => {
61                    // Discrete Laplacian: H = |Δp| / (2A)
62                    let mut laplacian = Vec3::ZERO;
63                    let mut area = 0.0f32;
64
65                    for &ni in neighbors {
66                        let q = mesh.vertices[ni as usize];
67                        laplacian += q - p;
68                        area += (q - p).length();
69                    }
70                    laplacian /= neighbors.len() as f32;
71                    let mixed_area = (area / neighbors.len() as f32).max(1e-10);
72                    values[vi] = laplacian.length() / (2.0 * mixed_area);
73                }
74                CurvatureType::Principal1 | CurvatureType::Principal2 => {
75                    // Approximate: H ± sqrt(H² - K)
76                    let h = {
77                        let mut lap = Vec3::ZERO;
78                        for &ni in neighbors { lap += mesh.vertices[ni as usize] - p; }
79                        lap /= neighbors.len() as f32;
80                        lap.length() * 0.5
81                    };
82                    let k = {
83                        let mut angle_sum = 0.0f32;
84                        let mut area = 0.0f32;
85                        for i in 0..neighbors.len() {
86                            let j = (i + 1) % neighbors.len();
87                            let a = mesh.vertices[neighbors[i] as usize] - p;
88                            let b = mesh.vertices[neighbors[j] as usize] - p;
89                            angle_sum += (a.dot(b) / (a.length() * b.length()).max(1e-10)).clamp(-1.0, 1.0).acos();
90                            area += a.cross(b).length() * 0.5;
91                        }
92                        (std::f32::consts::TAU - angle_sum) / (area / 3.0).max(1e-10)
93                    };
94                    let disc = (h * h - k).max(0.0).sqrt();
95                    values[vi] = if curvature_type == CurvatureType::Principal1 { h + disc } else { h - disc };
96                }
97            }
98        }
99
100        let min_value = values.iter().copied().fold(f32::MAX, f32::min);
101        let max_value = values.iter().copied().fold(f32::MIN, f32::max);
102
103        Self { curvature_type, values, min_value, max_value }
104    }
105
106    /// Map curvature values to colors and apply to mesh.
107    pub fn colorize_mesh(&self, mesh: &mut GeoMesh) {
108        let range = (self.max_value - self.min_value).max(1e-8);
109        mesh.colors.resize(mesh.vertices.len(), Vec4::ONE);
110        for (i, &val) in self.values.iter().enumerate() {
111            let t = ((val - self.min_value) / range).clamp(0.0, 1.0);
112            // Blue (low) → White (mid) → Red (high)
113            let color = if t < 0.5 {
114                let s = t * 2.0;
115                Vec4::new(s, s, 1.0, 1.0)
116            } else {
117                let s = (t - 0.5) * 2.0;
118                Vec4::new(1.0, 1.0 - s, 1.0 - s, 1.0)
119            };
120            mesh.colors[i] = color;
121        }
122    }
123}
124
125fn build_one_ring(mesh: &GeoMesh) -> HashMap<u32, Vec<u32>> {
126    let mut adj: HashMap<u32, Vec<u32>> = HashMap::new();
127    for tri in &mesh.triangles {
128        for &(a, b) in &[(tri.a, tri.b), (tri.b, tri.c), (tri.c, tri.a)] {
129            let entry = adj.entry(a).or_default();
130            if !entry.contains(&b) { entry.push(b); }
131            let entry = adj.entry(b).or_default();
132            if !entry.contains(&a) { entry.push(a); }
133        }
134    }
135    adj
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use glam::Vec2;
142    use crate::geometry::parametric::{Sphere, SurfaceGrid, ParametricSurface};
143
144    #[test]
145    fn sphere_gaussian_curvature_positive() {
146        let sphere = Sphere { radius: 1.0 };
147        let grid = SurfaceGrid::sample(&sphere, 10, 10);
148        let mesh = grid.to_mesh();
149        let curv = CurvatureField::compute(&mesh, CurvatureType::Gaussian);
150        // Sphere has positive Gaussian curvature everywhere
151        let positive_count = curv.values.iter().filter(|&&v| v > 0.0).count();
152        assert!(positive_count > curv.values.len() / 2);
153    }
154
155    #[test]
156    fn curvature_colorizes() {
157        let sphere = Sphere { radius: 1.0 };
158        let grid = SurfaceGrid::sample(&sphere, 6, 6);
159        let mut mesh = grid.to_mesh();
160        let curv = CurvatureField::compute(&mesh, CurvatureType::Mean);
161        curv.colorize_mesh(&mut mesh);
162        assert_eq!(mesh.colors.len(), mesh.vertices.len());
163    }
164}