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
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 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 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 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 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 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 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}