proof_engine/geometry/
curvature.rs1use glam::{Vec3, Vec4};
4use super::GeoMesh;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CurvatureType {
10 Gaussian,
11 Mean,
12 Principal1,
13 Principal2,
14}
15
16#[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 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 let adj = build_one_ring(mesh);
33 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 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 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 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 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 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 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}