1use ogeom_core::{OgeomResult, ogeom_bail};
42use ogeom_math::{Point, Vector};
43use ogeom_topo::Triangulation;
44
45use crate::point_index::{Neighbour, PointIndex};
46
47pub const NEIGHBOURS: usize = 100;
54
55#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Curvature {
58 pub mean: f64,
60 pub gaussian: f64,
62 pub k1: f64,
64 pub k2: f64,
66 pub dir1: Vector,
68 pub dir2: Vector,
71}
72
73pub fn curvature(mesh: &Triangulation, size: f64) -> OgeomResult<Vec<Option<Curvature>>> {
89 check_size(size)?;
90 let count = mesh.positions.len();
91 if let Some(&k) = mesh
92 .triangles
93 .iter()
94 .flatten()
95 .find(|&&k| k as usize >= count)
96 {
97 ogeom_bail!(Range, "a triangle names vertex {k} of {count}");
98 }
99 let normals = vertex_normals(&mesh.positions, mesh.triangles.iter().copied());
100 let members: Vec<u32> = (0..count)
101 .filter(|&v| normals[v].is_some())
102 .map(|v| u32::try_from(v).unwrap_or(u32::MAX))
103 .collect();
104 let cloud = Cloud::build(&mesh.positions, &normals, members)?;
105 let vertices: Vec<u32> = (0..count)
106 .map(|v| u32::try_from(v).unwrap_or(u32::MAX))
107 .collect();
108 Ok(in_parallel(&vertices, |v| {
109 normals[v as usize]?;
110 let p = mesh.positions[v as usize];
111 let near = cloud.near(p, size);
112 cloud.fit(p, &near, |_| true)?.curvature()
113 }))
114}
115
116pub(crate) fn check_size(size: f64) -> OgeomResult<()> {
118 if !size.is_finite() || size <= 0.0 {
119 ogeom_bail!(Numeric, "a neighbourhood size of {size} is not a length");
120 }
121 Ok(())
122}
123
124pub(crate) fn in_parallel<R: Send>(items: &[u32], f: impl Fn(u32) -> R + Sync) -> Vec<R> {
126 let batches: Vec<&[u32]> = items.chunks(crate::bvh::BATCH).collect();
127 ogeom_core::parallel::map_ordered(&batches, |_, batch| {
128 batch.iter().map(|&v| f(v)).collect::<Vec<R>>()
129 })
130 .into_iter()
131 .flatten()
132 .collect()
133}
134
135pub(crate) fn vertex_normals(
139 positions: &[Point],
140 faces: impl Iterator<Item = [u32; 3]>,
141) -> Vec<Option<Vector>> {
142 let mut sum = vec![Vector::new(0.0, 0.0, 0.0); positions.len()];
143 for [a, b, c] in faces {
144 let (pa, pb, pc) = (
145 positions[a as usize],
146 positions[b as usize],
147 positions[c as usize],
148 );
149 let n = (pb - pa).cross(pc - pa);
150 for v in [a, b, c] {
151 sum[v as usize] += n;
152 }
153 }
154 sum.into_iter()
155 .map(|n| {
156 let length = n.magnitude();
157 (length > 0.0 && length.is_finite()).then(|| n / length)
158 })
159 .collect()
160}
161
162pub(crate) struct Cloud<'a> {
164 index: PointIndex,
165 members: Vec<u32>,
167 positions: &'a [Point],
168 normals: &'a [Option<Vector>],
169}
170
171pub(crate) struct Near {
174 pub(crate) found: Vec<Neighbour>,
175 pub(crate) radius: f64,
176}
177
178impl<'a> Cloud<'a> {
179 pub(crate) fn build(
181 positions: &'a [Point],
182 normals: &'a [Option<Vector>],
183 members: Vec<u32>,
184 ) -> OgeomResult<Self> {
185 let points: Vec<Point> = members.iter().map(|&v| positions[v as usize]).collect();
186 Ok(Self {
187 index: PointIndex::build(&points)?,
188 members,
189 positions,
190 normals,
191 })
192 }
193
194 pub(crate) fn vertex(&self, slot: u32) -> u32 {
196 self.members[slot as usize]
197 }
198
199 pub(crate) fn near(&self, p: Point, size: f64) -> Near {
203 let mut found = self.index.k_nearest(p, NEIGHBOURS + 1, size);
204 let radius = if found.len() > NEIGHBOURS {
205 found.pop().map_or(size, |n| n.distance)
206 } else {
207 size
208 };
209 Near { found, radius }
210 }
211
212 pub(crate) fn fit(&self, p: Point, near: &Near, keep: impl Fn(u32) -> bool) -> Option<Local> {
215 let r = near.radius;
216 if r.is_nan() || r <= 0.0 {
217 return None;
218 }
219 let taken: Vec<(Point, Vector, f64)> = near
220 .found
221 .iter()
222 .filter_map(|n| {
223 let v = self.vertex(n.index);
224 let q = n.distance / r;
225 let w = (1.0 - q * q).powi(2);
226 let normal = self.normals[v as usize]?;
227 (q < 1.0 && keep(v)).then(|| (self.positions[v as usize], normal, w))
228 })
229 .collect();
230 let normal = taken
231 .iter()
232 .fold(Vector::new(0.0, 0.0, 0.0), |acc, &(_, n, w)| acc + n * w);
233 let length = normal.magnitude();
234 if length.is_nan() || length <= 0.0 {
235 return None;
236 }
237 let n = normal / length;
238 let (t1, t2) = tangents(n);
239 let mut m = [[0.0; 6]; 6];
240 let mut rhs = [0.0; 6];
241 for &(q, _, w) in &taken {
242 let d = q - p;
243 let (u, v, h) = (d.dot(t1) / r, d.dot(t2) / r, d.dot(n) / r);
244 let row = [1.0, u, v, u * u, u * v, v * v];
245 for i in 0..6 {
246 rhs[i] += w * row[i] * h;
247 for j in 0..=i {
248 m[i][j] += w * row[i] * row[j];
249 }
250 }
251 }
252 let c = cholesky_solve(&mut m, rhs)?;
253 Some(Local {
254 origin: p,
255 frame: [t1, t2, n],
256 height: c[0] * r,
257 gradient: [c[1], c[2]],
258 hessian: [2.0 * c[3] / r, c[4] / r, 2.0 * c[5] / r],
259 })
260 }
261}
262
263fn tangents(n: Vector) -> (Vector, Vector) {
265 let (ax, ay, az) = (n.x.abs(), n.y.abs(), n.z.abs());
266 let axis = if ax <= ay && ax <= az {
267 Vector::new(1.0, 0.0, 0.0)
268 } else if ay <= az {
269 Vector::new(0.0, 1.0, 0.0)
270 } else {
271 Vector::new(0.0, 0.0, 1.0)
272 };
273 let t1 = axis - n * axis.dot(n);
274 let t1 = t1 / t1.magnitude();
275 (t1, n.cross(t1))
276}
277
278fn cholesky_solve(m: &mut [[f64; 6]; 6], rhs: [f64; 6]) -> Option<[f64; 6]> {
282 let largest = (0..6).map(|i| m[i][i]).fold(0.0_f64, f64::max);
283 let floor = 6.0 * f64::EPSILON * largest;
284 for j in 0..6 {
285 let row = m[j];
286 let pivot = row[..j].iter().fold(row[j], |acc, &x| acc - x * x);
287 if pivot.is_nan() || pivot <= floor {
288 return None;
289 }
290 let root = pivot.sqrt();
291 m[j][j] = root;
292 let above = m[j];
293 for below in &mut m[j + 1..] {
294 let s = below[..j]
295 .iter()
296 .zip(&above[..j])
297 .fold(below[j], |acc, (&a, &b)| acc - a * b);
298 below[j] = s / root;
299 }
300 }
301 let mut y = rhs;
302 for i in 0..6 {
303 for k in 0..i {
304 y[i] -= m[i][k] * y[k];
305 }
306 y[i] /= m[i][i];
307 }
308 for i in (0..6).rev() {
309 for k in i + 1..6 {
310 y[i] -= m[k][i] * y[k];
311 }
312 y[i] /= m[i][i];
313 }
314 y.iter().all(|c| c.is_finite()).then_some(y)
315}
316
317pub(crate) struct Local {
319 origin: Point,
320 frame: [Vector; 3],
322 height: f64,
324 gradient: [f64; 2],
326 hessian: [f64; 3],
328}
329
330impl Local {
331 pub(crate) fn projected(&self) -> Point {
334 self.origin + self.frame[2] * self.height
335 }
336
337 pub(crate) fn curvature(&self) -> Option<Curvature> {
340 let [t1, t2, n] = self.frame;
341 let [fx, fy] = self.gradient;
342 let [fxx, fxy, fyy] = self.hessian;
343 let w = (1.0 + fx * fx + fy * fy).sqrt();
344 let (e, f, g) = (1.0 + fx * fx, fx * fy, 1.0 + fy * fy);
348 let (l, m, nn) = (-fxx / w, -fxy / w, -fyy / w);
349 let det = e * g - f * f;
350 let a = (g * l - f * m) / det;
352 let b = (g * m - f * nn) / det;
353 let c = (e * m - f * l) / det;
354 let d = (e * nn - f * m) / det;
355 let mean = 0.5 * (a + d);
356 let gaussian = (l * nn - m * m) / det;
357 let spread = (mean * mean - gaussian).max(0.0).sqrt();
360 let (k1, k2) = (mean + spread, mean - spread);
361 let (p, q) = ((b, k1 - a), (k1 - d, c));
364 let (u, v) = if p.0.hypot(p.1) >= q.0.hypot(q.1) {
365 p
366 } else {
367 q
368 };
369 let (u, v) = if u == 0.0 && v == 0.0 {
370 (1.0, 0.0)
371 } else {
372 (u, v)
373 };
374 let lifted = t1 * u + t2 * v + n * (fx * u + fy * v);
375 let dir1 = lifted / lifted.magnitude();
376 let normal = (n - t1 * fx - t2 * fy) / w;
377 let dir2 = normal.cross(dir1);
378 let dir2 = dir2 / dir2.magnitude();
379 let all = [
380 mean, gaussian, k1, k2, dir1.x, dir1.y, dir1.z, dir2.x, dir2.y, dir2.z,
381 ];
382 all.iter().all(|x| x.is_finite()).then_some(Curvature {
383 mean,
384 gaussian,
385 k1,
386 k2,
387 dir1,
388 dir2,
389 })
390 }
391}