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        // For each vertex, the other two corners of every triangle using it.
34        let mut vertex_tris: HashMap<u32, Vec<(u32, u32)>> = HashMap::new();
35        for tri in &mesh.triangles {
36            vertex_tris.entry(tri.a).or_default().push((tri.b, tri.c));
37            vertex_tris.entry(tri.b).or_default().push((tri.c, tri.a));
38            vertex_tris.entry(tri.c).or_default().push((tri.a, tri.b));
39        }
40
41        for vi in 0..n {
42            let p = mesh.vertices[vi];
43            let neighbors = match adj.get(&(vi as u32)) {
44                Some(n) => n,
45                None => continue,
46            };
47            if neighbors.is_empty() { continue; }
48
49            match curvature_type {
50                CurvatureType::Gaussian => {
51                    // Angle deficit method: K = (2 pi - sum of the angles at p
52                    // in its incident triangles) / (area / 3). The angles must
53                    // come from the actual triangles; pairing up one-ring
54                    // neighbours in insertion order (as this did) measured
55                    // angles across non-adjacent neighbours, so the sum was
56                    // wrong and a sphere came out mostly negative.
57                    let mut angle_sum = 0.0f32;
58                    let mut area = 0.0f32;
59                    for &(a_idx, b_idx) in vertex_tris.get(&(vi as u32)).map(|v| v.as_slice()).unwrap_or(&[]) {
60                        let a = mesh.vertices[a_idx as usize] - p;
61                        let b = mesh.vertices[b_idx as usize] - p;
62                        let cos_angle = a.dot(b) / (a.length() * b.length()).max(1e-10);
63                        angle_sum += cos_angle.clamp(-1.0, 1.0).acos();
64                        area += a.cross(b).length() * 0.5;
65                    }
66                    let mixed_area = (area / 3.0).max(1e-10);
67                    values[vi] = (std::f32::consts::TAU - angle_sum) / mixed_area;
68                }
69                CurvatureType::Mean => {
70                    // Discrete Laplacian: H = |Δp| / (2A)
71                    let mut laplacian = Vec3::ZERO;
72                    let mut area = 0.0f32;
73
74                    for &ni in neighbors {
75                        let q = mesh.vertices[ni as usize];
76                        laplacian += q - p;
77                        area += (q - p).length();
78                    }
79                    laplacian /= neighbors.len() as f32;
80                    let mixed_area = (area / neighbors.len() as f32).max(1e-10);
81                    values[vi] = laplacian.length() / (2.0 * mixed_area);
82                }
83                CurvatureType::Principal1 | CurvatureType::Principal2 => {
84                    // Approximate: H ± sqrt(H² - K)
85                    let h = {
86                        let mut lap = Vec3::ZERO;
87                        for &ni in neighbors { lap += mesh.vertices[ni as usize] - p; }
88                        lap /= neighbors.len() as f32;
89                        lap.length() * 0.5
90                    };
91                    let k = {
92                        let mut angle_sum = 0.0f32;
93                        let mut area = 0.0f32;
94                        for i in 0..neighbors.len() {
95                            let j = (i + 1) % neighbors.len();
96                            let a = mesh.vertices[neighbors[i] as usize] - p;
97                            let b = mesh.vertices[neighbors[j] as usize] - p;
98                            angle_sum += (a.dot(b) / (a.length() * b.length()).max(1e-10)).clamp(-1.0, 1.0).acos();
99                            area += a.cross(b).length() * 0.5;
100                        }
101                        (std::f32::consts::TAU - angle_sum) / (area / 3.0).max(1e-10)
102                    };
103                    let disc = (h * h - k).max(0.0).sqrt();
104                    values[vi] = if curvature_type == CurvatureType::Principal1 { h + disc } else { h - disc };
105                }
106            }
107        }
108
109        let min_value = values.iter().copied().fold(f32::MAX, f32::min);
110        let max_value = values.iter().copied().fold(f32::MIN, f32::max);
111
112        Self { curvature_type, values, min_value, max_value }
113    }
114
115    /// Map curvature values to colors and apply to mesh.
116    pub fn colorize_mesh(&self, mesh: &mut GeoMesh) {
117        let range = (self.max_value - self.min_value).max(1e-8);
118        mesh.colors.resize(mesh.vertices.len(), Vec4::ONE);
119        for (i, &val) in self.values.iter().enumerate() {
120            let t = ((val - self.min_value) / range).clamp(0.0, 1.0);
121            // Blue (low) → White (mid) → Red (high)
122            let color = if t < 0.5 {
123                let s = t * 2.0;
124                Vec4::new(s, s, 1.0, 1.0)
125            } else {
126                let s = (t - 0.5) * 2.0;
127                Vec4::new(1.0, 1.0 - s, 1.0 - s, 1.0)
128            };
129            mesh.colors[i] = color;
130        }
131    }
132}
133
134fn build_one_ring(mesh: &GeoMesh) -> HashMap<u32, Vec<u32>> {
135    let mut adj: HashMap<u32, Vec<u32>> = HashMap::new();
136    for tri in &mesh.triangles {
137        for &(a, b) in &[(tri.a, tri.b), (tri.b, tri.c), (tri.c, tri.a)] {
138            let entry = adj.entry(a).or_default();
139            if !entry.contains(&b) { entry.push(b); }
140            let entry = adj.entry(b).or_default();
141            if !entry.contains(&a) { entry.push(a); }
142        }
143    }
144    adj
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use glam::Vec2;
151    use crate::geometry::parametric::{Sphere, SurfaceGrid, ParametricSurface};
152
153    #[test]
154    fn sphere_gaussian_curvature_positive() {
155        let sphere = Sphere { radius: 1.0 };
156        let grid = SurfaceGrid::sample(&sphere, 10, 10);
157        let mesh = grid.to_mesh();
158        let curv = CurvatureField::compute(&mesh, CurvatureType::Gaussian);
159        // Sphere has positive Gaussian curvature everywhere
160        let positive_count = curv.values.iter().filter(|&&v| v > 0.0).count();
161        assert!(positive_count > curv.values.len() / 2);
162    }
163
164    #[test]
165    fn curvature_colorizes() {
166        let sphere = Sphere { radius: 1.0 };
167        let grid = SurfaceGrid::sample(&sphere, 6, 6);
168        let mut mesh = grid.to_mesh();
169        let curv = CurvatureField::compute(&mesh, CurvatureType::Mean);
170        curv.colorize_mesh(&mut mesh);
171        assert_eq!(mesh.colors.len(), mesh.vertices.len());
172    }
173}