1use crate::face_op::get_axis_aligned_projection;
21use crate::impl_mesh::ManifoldImpl;
22use crate::linalg::{cross, dot, length, IVec3, Vec3};
23use crate::math;
24use crate::polygon::ccw;
25use crate::types::K_TWO_PI;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Property {
30 SurfaceArea,
31 Volume,
32}
33
34impl ManifoldImpl {
35 pub fn get_property(&self, prop: Property) -> f64 {
38 if self.is_empty() {
39 return 0.0;
40 }
41
42 let mut value: f64 = 0.0;
43 let mut compensation: f64 = 0.0;
44
45 for tri in 0..self.num_tri() {
46 let sv = self.halfedge[3 * tri].start_vert;
47 if sv < 0 {
48 continue;
49 }
50 let v0 = self.vert_pos[sv as usize];
51 let v1 = self.vert_pos[self.halfedge[3 * tri + 1].start_vert as usize];
52 let v2 = self.vert_pos[self.halfedge[3 * tri + 2].start_vert as usize];
53
54 let value1 = match prop {
55 Property::Volume => {
56 let cross_p = cross(v1 - v0, v2 - v0);
57 dot(cross_p, v0) / 6.0
58 }
59 Property::SurfaceArea => {
60 length(cross(v1 - v0, v2 - v0)) / 2.0
61 }
62 };
63
64 let t = value + value1;
65 compensation += (value - t) + value1;
66 value = t;
67 }
68 value + compensation
69 }
70
71 pub fn matches_tri_normals(&self) -> bool {
73 if self.halfedge.is_empty() || self.face_normal.len() != self.num_tri() {
74 return true;
75 }
76 for face in 0..self.num_tri() {
77 if self.halfedge[3 * face].paired_halfedge < 0 {
78 continue;
79 }
80
81 let projection = get_axis_aligned_projection(self.face_normal[face]);
82 let mut v = [crate::linalg::Vec2::new(0.0, 0.0); 3];
83 let mut max_d = f64::NEG_INFINITY;
84 let mut min_d = f64::INFINITY;
85 let mut any_non_finite = false;
86
87 for i in 0..3 {
88 let p = self.vert_pos[self.halfedge[3 * face + i].start_vert as usize];
89 v[i] = projection.apply(p);
90 let d = dot(p, self.face_normal[face]);
91 if !d.is_finite() {
92 any_non_finite = true;
93 break;
94 }
95 max_d = max_d.max(d);
96 min_d = min_d.min(d);
97 }
98
99 if any_non_finite {
100 continue;
101 }
102 if max_d - min_d > 2.0 * self.tolerance {
103 return false;
104 }
105
106 let winding = ccw(v[0], v[1], v[2], self.epsilon * 2.0);
107 if winding < 0 {
108 return false;
109 }
110 }
111 true
112 }
113
114 pub fn num_degenerate_tris(&self) -> i32 {
116 if self.halfedge.is_empty() || self.face_normal.len() != self.num_tri() {
117 return 0;
118 }
119 let mut count = 0i32;
120 for face in 0..self.num_tri() {
121 if self.halfedge[3 * face].paired_halfedge < 0 {
122 count += 1;
123 continue;
124 }
125
126 let projection = get_axis_aligned_projection(self.face_normal[face]);
127 let mut v = [crate::linalg::Vec2::new(0.0, 0.0); 3];
128 for i in 0..3 {
129 v[i] = projection.apply(
130 self.vert_pos[self.halfedge[3 * face + i].start_vert as usize],
131 );
132 }
133
134 let winding = ccw(v[0], v[1], v[2], self.tolerance / 2.0);
136 if winding == 0 {
137 count += 1;
138 }
139 }
140 count
141 }
142
143 pub fn is_convex(&self) -> bool {
145 let chi = self.num_vert() as i64 - self.num_edge() as i64 + self.num_tri() as i64;
146 let genus = 1 - chi / 2;
147 if genus != 0 {
148 return false;
149 }
150
151 let nb_edges = self.halfedge.len();
152 for idx in 0..nb_edges {
153 let edge = &self.halfedge[idx];
154 if !edge.is_forward() {
155 continue;
156 }
157
158 let normal0 = self.face_normal[idx / 3];
159 let normal1 = self.face_normal[edge.paired_halfedge as usize / 3];
160
161 if normal0 == normal1 {
162 continue;
163 }
164
165 let edge_vec =
166 self.vert_pos[edge.end_vert as usize] - self.vert_pos[edge.start_vert as usize];
167 let convex = dot(edge_vec, cross(normal0, normal1)) > 0.0;
168 if !convex {
169 return false;
170 }
171 }
172 true
173 }
174
175 pub fn calculate_curvature(&mut self, gaussian_idx: i32, mean_idx: i32) {
178 if self.is_empty() {
179 return;
180 }
181 if gaussian_idx < 0 && mean_idx < 0 {
182 return;
183 }
184
185 let num_vert = self.num_vert();
186 let mut mean_curvature = vec![0.0f64; num_vert];
187 let mut gaussian_curvature = vec![K_TWO_PI; num_vert];
188 let mut area = vec![0.0f64; num_vert];
189 let mut degree = vec![0.0f64; num_vert];
190
191 for tri in 0..self.num_tri() {
192 let mut edge_dirs = [Vec3::new(0.0, 0.0, 0.0); 3];
193 let mut edge_length = [0.0f64; 3];
194
195 for i in 0..3 {
196 let start_vert = self.halfedge[3 * tri + i].start_vert as usize;
197 let end_vert = self.halfedge[3 * tri + i].end_vert as usize;
198 edge_dirs[i] = self.vert_pos[end_vert] - self.vert_pos[start_vert];
199 edge_length[i] = length(edge_dirs[i]);
200 if edge_length[i] > 0.0 {
201 edge_dirs[i] = edge_dirs[i] / edge_length[i];
202 }
203
204 let neighbor_tri = self.halfedge[3 * tri + i].paired_halfedge as usize / 3;
205 let dihedral = 0.25
206 * edge_length[i]
207 * math::asin(dot(
208 cross(self.face_normal[tri], self.face_normal[neighbor_tri]),
209 edge_dirs[i],
210 ));
211 mean_curvature[start_vert] += dihedral;
212 mean_curvature[end_vert] += dihedral;
213 degree[start_vert] += 1.0;
214 }
215
216 let phi0 = math::acos(-dot(edge_dirs[2], edge_dirs[0]));
217 let phi1 = math::acos(-dot(edge_dirs[0], edge_dirs[1]));
218 let phi2 = std::f64::consts::PI - phi0 - phi1;
219 let area3 = edge_length[0] * edge_length[1] * length(cross(edge_dirs[0], edge_dirs[1]))
220 / 6.0;
221
222 let phi = [phi0, phi1, phi2];
223 for i in 0..3 {
224 let vert = self.halfedge[3 * tri + i].start_vert as usize;
225 gaussian_curvature[vert] -= phi[i];
226 area[vert] += area3;
227 }
228 }
229
230 for vert in 0..num_vert {
231 let factor = degree[vert] / (6.0 * area[vert]);
232 mean_curvature[vert] *= factor;
233 gaussian_curvature[vert] *= factor;
234 }
235
236 let old_num_prop = self.num_prop;
237 let num_prop = old_num_prop
238 .max(if gaussian_idx >= 0 { gaussian_idx as usize + 1 } else { 0 })
239 .max(if mean_idx >= 0 { mean_idx as usize + 1 } else { 0 });
240
241 let old_properties = self.properties.clone();
242 let num_prop_vert = self.num_prop_vert();
243 self.properties = vec![0.0f64; num_prop * num_prop_vert];
244 self.num_prop = num_prop;
245
246 let mut visited = vec![false; num_prop_vert];
247
248 for tri in 0..self.num_tri() {
249 for i in 0..3 {
250 let edge = &self.halfedge[3 * tri + i];
251 let vert = edge.start_vert as usize;
252 let prop_vert = edge.prop_vert as usize;
253
254 if visited[prop_vert] {
255 continue;
256 }
257 visited[prop_vert] = true;
258
259 for p in 0..old_num_prop {
260 self.properties[num_prop * prop_vert + p] =
261 old_properties[old_num_prop * prop_vert + p];
262 }
263
264 if gaussian_idx >= 0 {
265 self.properties[num_prop * prop_vert + gaussian_idx as usize] =
266 gaussian_curvature[vert];
267 }
268 if mean_idx >= 0 {
269 self.properties[num_prop * prop_vert + mean_idx as usize] =
270 mean_curvature[vert];
271 }
272 }
273 }
274 }
275
276 pub fn is_index_in_bounds(&self, tri_verts: &[IVec3]) -> bool {
279 if tri_verts.is_empty() {
280 return true;
281 }
282 let num_vert = self.num_vert() as i32;
283 for tri in tri_verts {
284 let min_v = tri.x.min(tri.y).min(tri.z);
285 let max_v = tri.x.max(tri.y).max(tri.z);
286 if min_v < 0 || max_v >= num_vert {
287 return false;
288 }
289 }
290 true
291 }
292}
293
294#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::linalg::Mat3x4;
302
303 #[test]
304 fn test_tetrahedron_volume() {
305 let m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
306 let vol = m.get_property(Property::Volume);
307 assert!(
310 (vol.abs() - 8.0 / 3.0).abs() < 1e-10,
311 "Expected volume ~2.6667, got {}",
312 vol
313 );
314 }
315
316 #[test]
317 fn test_cube_volume() {
318 let m = ManifoldImpl::cube(&Mat3x4::identity());
319 let vol = m.get_property(Property::Volume);
320 assert!(
321 (vol.abs() - 1.0).abs() < 1e-10,
322 "Expected unit cube volume = 1.0, got {}",
323 vol
324 );
325 }
326
327 #[test]
328 fn test_cube_surface_area() {
329 let m = ManifoldImpl::cube(&Mat3x4::identity());
330 let area = m.get_property(Property::SurfaceArea);
331 assert!(
332 (area - 6.0).abs() < 1e-10,
333 "Expected unit cube surface area = 6.0, got {}",
334 area
335 );
336 }
337
338 #[test]
339 fn test_octahedron_volume() {
340 let m = ManifoldImpl::octahedron(&Mat3x4::identity());
341 let vol = m.get_property(Property::Volume);
342 assert!(
344 (vol.abs() - 4.0 / 3.0).abs() < 1e-10,
345 "Expected octahedron volume ~1.3333, got {}",
346 vol
347 );
348 }
349
350 #[test]
351 fn test_octahedron_surface_area() {
352 let m = ManifoldImpl::octahedron(&Mat3x4::identity());
353 let area = m.get_property(Property::SurfaceArea);
354 let expected = 4.0 * 3.0_f64.sqrt();
357 assert!(
358 (area - expected).abs() < 1e-10,
359 "Expected octahedron surface area ~{}, got {}",
360 expected,
361 area
362 );
363 }
364
365 #[test]
366 fn test_empty_mesh_properties() {
367 let m = ManifoldImpl::new();
368 assert_eq!(m.get_property(Property::Volume), 0.0);
369 assert_eq!(m.get_property(Property::SurfaceArea), 0.0);
370 }
371
372 #[test]
373 fn test_matches_tri_normals_cube() {
374 let m = ManifoldImpl::cube(&Mat3x4::identity());
375 assert!(
376 m.matches_tri_normals(),
377 "Cube should have CCW triangles matching normals"
378 );
379 }
380
381 #[test]
382 fn test_matches_tri_normals_tetrahedron() {
383 let m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
384 assert!(
385 m.matches_tri_normals(),
386 "Tetrahedron should have CCW triangles matching normals"
387 );
388 }
389
390 #[test]
391 fn test_num_degenerate_tris_cube() {
392 let m = ManifoldImpl::cube(&Mat3x4::identity());
393 assert_eq!(
394 m.num_degenerate_tris(),
395 0,
396 "Cube should have no degenerate triangles"
397 );
398 }
399
400 #[test]
401 fn test_num_degenerate_tris_tetrahedron() {
402 let m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
403 assert_eq!(
404 m.num_degenerate_tris(),
405 0,
406 "Tetrahedron should have no degenerate triangles"
407 );
408 }
409
410 #[test]
411 fn test_is_convex_cube() {
412 let m = ManifoldImpl::cube(&Mat3x4::identity());
413 assert!(m.is_convex(), "Unit cube should be convex");
414 }
415
416 #[test]
417 fn test_is_convex_tetrahedron() {
418 let m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
419 assert!(m.is_convex(), "Tetrahedron should be convex");
420 }
421
422 #[test]
423 fn test_is_convex_octahedron() {
424 let m = ManifoldImpl::octahedron(&Mat3x4::identity());
425 assert!(m.is_convex(), "Octahedron should be convex");
426 }
427
428 #[test]
429 fn test_is_index_in_bounds() {
430 let m = ManifoldImpl::cube(&Mat3x4::identity());
431 let valid = vec![IVec3::new(0, 1, 2), IVec3::new(3, 4, 5)];
432 assert!(m.is_index_in_bounds(&valid));
433
434 let invalid = vec![IVec3::new(0, 1, 100)];
435 assert!(!m.is_index_in_bounds(&invalid));
436
437 let negative = vec![IVec3::new(-1, 0, 1)];
438 assert!(!m.is_index_in_bounds(&negative));
439
440 assert!(m.is_index_in_bounds(&[]));
441 }
442
443 #[test]
444 fn test_calculate_curvature_cube() {
445 let mut m = ManifoldImpl::cube(&Mat3x4::identity());
446 m.calculate_curvature(0, 1);
447 assert_eq!(m.num_prop, 2);
448 assert!(!m.properties.is_empty(), "Properties should be populated");
449 }
450
451 #[test]
452 fn test_calculate_curvature_skip_both() {
453 let mut m = ManifoldImpl::cube(&Mat3x4::identity());
454 let old_props = m.properties.clone();
455 let old_num_prop = m.num_prop;
456 m.calculate_curvature(-1, -1);
457 assert_eq!(m.num_prop, old_num_prop);
458 assert_eq!(m.properties, old_props);
459 }
460
461 #[test]
462 fn test_scaled_cube_volume() {
463 use crate::linalg::{mat4_to_mat3x4, scaling_matrix};
464 let scale = Vec3::new(2.0, 3.0, 4.0);
465 let t = mat4_to_mat3x4(scaling_matrix(scale));
466 let m = ManifoldImpl::cube(&t);
467 let vol = m.get_property(Property::Volume);
468 assert!(
469 (vol.abs() - 24.0).abs() < 1e-10,
470 "Expected 2×3×4 cube volume = 24.0, got {}",
471 vol
472 );
473 }
474
475 #[test]
476 fn test_scaled_cube_surface_area() {
477 use crate::linalg::{mat4_to_mat3x4, scaling_matrix};
478 let scale = Vec3::new(2.0, 3.0, 4.0);
479 let t = mat4_to_mat3x4(scaling_matrix(scale));
480 let m = ManifoldImpl::cube(&t);
481 let area = m.get_property(Property::SurfaceArea);
482 assert!(
484 (area - 52.0).abs() < 1e-10,
485 "Expected 2×3×4 cube surface area = 52.0, got {}",
486 area
487 );
488 }
489}