Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
//! 3D Delaunay tetrahedralization.
|
||||
|
||||
mod predicates;
|
||||
mod triangulate;
|
||||
|
||||
pub use predicates::{in_circumsphere, orient3d};
|
||||
pub use triangulate::delaunay_3d;
|
||||
@@ -0,0 +1,180 @@
|
||||
//! Geometric predicates for Delaunay triangulation.
|
||||
//!
|
||||
//! These predicates determine orientation and circumsphere tests.
|
||||
//! For production use, these should be replaced with adaptive-precision
|
||||
//! or exact arithmetic implementations (e.g., from the `robust` crate).
|
||||
|
||||
use nalgebra::Point3;
|
||||
|
||||
/// Compute the orientation of four points in 3D.
|
||||
///
|
||||
/// Returns:
|
||||
/// - Positive if d is on the positive side of the plane defined by (a,b,c)
|
||||
/// - Negative if on the negative side
|
||||
/// - Zero if coplanar
|
||||
pub fn orient3d(a: &Point3<f64>, b: &Point3<f64>, c: &Point3<f64>, d: &Point3<f64>) -> f64 {
|
||||
// Standard orient3d using the signed volume of the tetrahedron
|
||||
// Positive when d is above the plane (a,b,c) with ccw orientation
|
||||
let ab = b - a;
|
||||
let ac = c - a;
|
||||
let ad = d - a;
|
||||
|
||||
// det = ab · (ac × ad)
|
||||
ab.dot(&ac.cross(&ad))
|
||||
}
|
||||
|
||||
/// Test if point e is inside the circumsphere of tetrahedron (a,b,c,d).
|
||||
///
|
||||
/// Returns:
|
||||
/// - Positive if e is inside the circumsphere
|
||||
/// - Negative if outside
|
||||
/// - Zero if on the sphere
|
||||
///
|
||||
/// Assumes (a,b,c,d) has positive orientation (orient3d > 0).
|
||||
pub fn in_circumsphere(
|
||||
a: &Point3<f64>,
|
||||
b: &Point3<f64>,
|
||||
c: &Point3<f64>,
|
||||
d: &Point3<f64>,
|
||||
e: &Point3<f64>,
|
||||
) -> f64 {
|
||||
// InSphere determinant test
|
||||
// If orient3d(a,b,c,d) > 0, then insphere(a,b,c,d,e) > 0 means e is inside
|
||||
|
||||
let aex = a.x - e.x;
|
||||
let aey = a.y - e.y;
|
||||
let aez = a.z - e.z;
|
||||
let bex = b.x - e.x;
|
||||
let bey = b.y - e.y;
|
||||
let bez = b.z - e.z;
|
||||
let cex = c.x - e.x;
|
||||
let cey = c.y - e.y;
|
||||
let cez = c.z - e.z;
|
||||
let dex = d.x - e.x;
|
||||
let dey = d.y - e.y;
|
||||
let dez = d.z - e.z;
|
||||
|
||||
let ae_sq = aex * aex + aey * aey + aez * aez;
|
||||
let be_sq = bex * bex + bey * bey + bez * bez;
|
||||
let ce_sq = cex * cex + cey * cey + cez * cez;
|
||||
let de_sq = dex * dex + dey * dey + dez * dez;
|
||||
|
||||
// 4x4 determinant using cofactor expansion
|
||||
let ab = aex * bey - bex * aey;
|
||||
let bc = bex * cey - cex * bey;
|
||||
let cd = cex * dey - dex * cey;
|
||||
let da = dex * aey - aex * dey;
|
||||
let ac = aex * cey - cex * aey;
|
||||
let bd = bex * dey - dex * bey;
|
||||
|
||||
let abc = aez * bc - bez * ac + cez * ab;
|
||||
let bcd = bez * cd - cez * bd + dez * bc;
|
||||
let cda = cez * da + dez * ac + aez * cd;
|
||||
let dab = dez * ab + aez * bd + bez * da;
|
||||
|
||||
ae_sq * bcd - be_sq * cda + ce_sq * dab - de_sq * abc
|
||||
}
|
||||
|
||||
/// Compute the circumcenter of a tetrahedron.
|
||||
pub fn circumcenter(
|
||||
a: &Point3<f64>,
|
||||
b: &Point3<f64>,
|
||||
c: &Point3<f64>,
|
||||
d: &Point3<f64>,
|
||||
) -> Option<Point3<f64>> {
|
||||
let ba = b - a;
|
||||
let ca = c - a;
|
||||
let da = d - a;
|
||||
|
||||
let ba_sq = ba.norm_squared();
|
||||
let ca_sq = ca.norm_squared();
|
||||
let da_sq = da.norm_squared();
|
||||
|
||||
// Determinant (denominator)
|
||||
let denom = 2.0 * ba.dot(&ca.cross(&da));
|
||||
|
||||
if denom.abs() < 1e-15 {
|
||||
return None; // Degenerate tetrahedron
|
||||
}
|
||||
|
||||
// Circumcenter = a + (ba² * (ca × da) + ca² * (da × ba) + da² * (ba × ca)) / denom
|
||||
let cc = a + (ca.cross(&da) * ba_sq + da.cross(&ba) * ca_sq + ba.cross(&ca) * da_sq) / denom;
|
||||
|
||||
Some(cc)
|
||||
}
|
||||
|
||||
/// Compute the circumradius of a tetrahedron.
|
||||
pub fn circumradius(
|
||||
a: &Point3<f64>,
|
||||
b: &Point3<f64>,
|
||||
c: &Point3<f64>,
|
||||
d: &Point3<f64>,
|
||||
) -> Option<f64> {
|
||||
let cc = circumcenter(a, b, c, d)?;
|
||||
Some((a - cc).norm())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_orient3d() {
|
||||
let a = Point3::new(0.0, 0.0, 0.0);
|
||||
let b = Point3::new(1.0, 0.0, 0.0);
|
||||
let c = Point3::new(0.0, 1.0, 0.0);
|
||||
let d = Point3::new(0.0, 0.0, 1.0);
|
||||
|
||||
// d is above the plane (a,b,c)
|
||||
let o = orient3d(&a, &b, &c, &d);
|
||||
assert!(o > 0.0);
|
||||
|
||||
// Flip d below
|
||||
let d_below = Point3::new(0.0, 0.0, -1.0);
|
||||
let o2 = orient3d(&a, &b, &c, &d_below);
|
||||
assert!(o2 < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_circumsphere() {
|
||||
// Regular tetrahedron
|
||||
let a = Point3::new(1.0, 1.0, 1.0);
|
||||
let b = Point3::new(-1.0, -1.0, 1.0);
|
||||
let c = Point3::new(-1.0, 1.0, -1.0);
|
||||
let d = Point3::new(1.0, -1.0, -1.0);
|
||||
|
||||
// Center should be inside
|
||||
let center = Point3::new(0.0, 0.0, 0.0);
|
||||
let inside = in_circumsphere(&a, &b, &c, &d, ¢er);
|
||||
assert!(inside > 0.0);
|
||||
|
||||
// Far point should be outside
|
||||
let far = Point3::new(10.0, 10.0, 10.0);
|
||||
let outside = in_circumsphere(&a, &b, &c, &d, &far);
|
||||
assert!(outside < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_circumcenter() {
|
||||
// Regular tetrahedron centered at origin
|
||||
let a = Point3::new(1.0, 1.0, 1.0);
|
||||
let b = Point3::new(-1.0, -1.0, 1.0);
|
||||
let c = Point3::new(-1.0, 1.0, -1.0);
|
||||
let d = Point3::new(1.0, -1.0, -1.0);
|
||||
|
||||
let cc = circumcenter(&a, &b, &c, &d).unwrap();
|
||||
|
||||
// Center should be near origin
|
||||
assert!(cc.coords.norm() < 0.01);
|
||||
|
||||
// All vertices should be equidistant from circumcenter
|
||||
let ra = (a - cc).norm();
|
||||
let rb = (b - cc).norm();
|
||||
let rc = (c - cc).norm();
|
||||
let rd = (d - cc).norm();
|
||||
|
||||
assert!((ra - rb).abs() < 1e-10);
|
||||
assert!((ra - rc).abs() < 1e-10);
|
||||
assert!((ra - rd).abs() < 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
//! 3D Delaunay tetrahedralization using Bowyer-Watson algorithm.
|
||||
|
||||
use super::predicates::{in_circumsphere, orient3d};
|
||||
use crate::error::{MeshGenError, Result};
|
||||
use crate::mesh::{TetrahedralMesh, Tetrahedron, Vertex};
|
||||
use nalgebra::Point3;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Compute 3D Delaunay tetrahedralization of a point set.
|
||||
///
|
||||
/// Uses the incremental Bowyer-Watson algorithm.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `points` - Points to triangulate
|
||||
///
|
||||
/// # Returns
|
||||
/// A tetrahedral mesh
|
||||
pub fn delaunay_3d(points: &[Point3<f64>]) -> Result<TetrahedralMesh> {
|
||||
if points.len() < 4 {
|
||||
return Err(MeshGenError::InvalidInput(
|
||||
"At least 4 points required for tetrahedralization".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Compute bounding box
|
||||
let (min_pt, max_pt) = bounding_box(points);
|
||||
let span = max_pt - min_pt;
|
||||
let max_span = span.x.max(span.y).max(span.z);
|
||||
|
||||
// Create super-tetrahedron that contains all points
|
||||
// Make it large enough with some margin
|
||||
let center = Point3::from((min_pt.coords + max_pt.coords) / 2.0);
|
||||
let size = max_span * 10.0; // Large enough to contain all points
|
||||
|
||||
let super_tet = create_super_tetrahedron(¢er, size);
|
||||
|
||||
// Initialize triangulation with super-tetrahedron
|
||||
let mut vertices: Vec<Point3<f64>> = super_tet.to_vec();
|
||||
let mut tetrahedra: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
|
||||
|
||||
// Insert points one by one
|
||||
for (pi, &point) in points.iter().enumerate() {
|
||||
let vi = vertices.len();
|
||||
vertices.push(point);
|
||||
|
||||
// Find all tetrahedra whose circumsphere contains the new point
|
||||
let mut bad_tets: HashSet<usize> = HashSet::new();
|
||||
for (ti, tet) in tetrahedra.iter().enumerate() {
|
||||
let a = &vertices[tet[0]];
|
||||
let b = &vertices[tet[1]];
|
||||
let c = &vertices[tet[2]];
|
||||
let d = &vertices[tet[3]];
|
||||
|
||||
// Check orientation and adjust if needed
|
||||
let orient = orient3d(a, b, c, d);
|
||||
if orient.abs() < 1e-15 {
|
||||
continue; // Degenerate, skip
|
||||
}
|
||||
|
||||
let test = if orient > 0.0 {
|
||||
in_circumsphere(a, b, c, d, &point)
|
||||
} else {
|
||||
in_circumsphere(a, b, d, c, &point)
|
||||
};
|
||||
|
||||
if test > 0.0 {
|
||||
bad_tets.insert(ti);
|
||||
}
|
||||
}
|
||||
|
||||
if bad_tets.is_empty() {
|
||||
// Point might be outside all circumspheres (degenerate case)
|
||||
// Skip this point
|
||||
vertices.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the boundary of the cavity (faces not shared by bad tets)
|
||||
let boundary_faces = find_cavity_boundary(&tetrahedra, &bad_tets);
|
||||
|
||||
// Remove bad tetrahedra (mark for removal, will compact later)
|
||||
let mut new_tets: Vec<[usize; 4]> = Vec::new();
|
||||
for (ti, tet) in tetrahedra.iter().enumerate() {
|
||||
if !bad_tets.contains(&ti) {
|
||||
new_tets.push(*tet);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new tetrahedra by connecting boundary faces to new point
|
||||
for face in boundary_faces {
|
||||
// Ensure correct orientation
|
||||
let a = &vertices[face[0]];
|
||||
let b = &vertices[face[1]];
|
||||
let c = &vertices[face[2]];
|
||||
let p = &vertices[vi];
|
||||
|
||||
let orient = orient3d(a, b, c, p);
|
||||
let new_tet = if orient > 0.0 {
|
||||
[face[0], face[1], face[2], vi]
|
||||
} else {
|
||||
[face[0], face[2], face[1], vi]
|
||||
};
|
||||
|
||||
new_tets.push(new_tet);
|
||||
}
|
||||
|
||||
tetrahedra = new_tets;
|
||||
|
||||
// Progress reporting for large point sets
|
||||
if points.len() > 1000 && pi % 1000 == 0 {
|
||||
// Could add callback here for progress
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tetrahedra connected to super-tetrahedron vertices (indices 0-3)
|
||||
let super_indices: HashSet<usize> = [0, 1, 2, 3].into_iter().collect();
|
||||
tetrahedra.retain(|tet| !tet.iter().any(|&vi| super_indices.contains(&vi)));
|
||||
|
||||
// Compact vertices (remove super-tet vertices and reindex)
|
||||
let mut mesh = TetrahedralMesh::new();
|
||||
let mut old_to_new: HashMap<usize, usize> = HashMap::new();
|
||||
|
||||
for (old_idx, &point) in vertices.iter().enumerate().skip(4) {
|
||||
let new_idx = mesh.add_vertex(Vertex::new(point.x, point.y, point.z));
|
||||
old_to_new.insert(old_idx, new_idx);
|
||||
}
|
||||
|
||||
for tet in tetrahedra {
|
||||
if let (Some(&v0), Some(&v1), Some(&v2), Some(&v3)) = (
|
||||
old_to_new.get(&tet[0]),
|
||||
old_to_new.get(&tet[1]),
|
||||
old_to_new.get(&tet[2]),
|
||||
old_to_new.get(&tet[3]),
|
||||
) {
|
||||
mesh.add_tetrahedron(Tetrahedron::new(v0, v1, v2, v3));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
/// Create a super-tetrahedron that contains all points.
|
||||
fn create_super_tetrahedron(center: &Point3<f64>, size: f64) -> [Point3<f64>; 4] {
|
||||
// Create a large regular tetrahedron centered at `center`
|
||||
let h = size * 2.0; // Height from center to vertex
|
||||
|
||||
[
|
||||
Point3::new(center.x, center.y + h, center.z),
|
||||
Point3::new(center.x - h * 0.866, center.y - h * 0.5, center.z - h * 0.5),
|
||||
Point3::new(center.x + h * 0.866, center.y - h * 0.5, center.z - h * 0.5),
|
||||
Point3::new(center.x, center.y - h * 0.5, center.z + h),
|
||||
]
|
||||
}
|
||||
|
||||
/// Find boundary faces of the cavity formed by bad tetrahedra.
|
||||
fn find_cavity_boundary(tetrahedra: &[[usize; 4]], bad_tets: &HashSet<usize>) -> Vec<[usize; 3]> {
|
||||
let mut face_count: HashMap<[usize; 3], usize> = HashMap::new();
|
||||
let mut face_original: HashMap<[usize; 3], [usize; 3]> = HashMap::new();
|
||||
|
||||
for &ti in bad_tets {
|
||||
let tet = tetrahedra[ti];
|
||||
let faces = [
|
||||
[tet[1], tet[2], tet[3]],
|
||||
[tet[0], tet[3], tet[2]],
|
||||
[tet[0], tet[1], tet[3]],
|
||||
[tet[0], tet[2], tet[1]],
|
||||
];
|
||||
|
||||
for face in faces {
|
||||
let mut sorted = face;
|
||||
sorted.sort_unstable();
|
||||
|
||||
*face_count.entry(sorted).or_insert(0) += 1;
|
||||
face_original.entry(sorted).or_insert(face);
|
||||
}
|
||||
}
|
||||
|
||||
// Boundary faces appear exactly once
|
||||
face_count
|
||||
.into_iter()
|
||||
.filter(|(_, count)| *count == 1)
|
||||
.map(|(sorted, _)| *face_original.get(&sorted).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute bounding box of points.
|
||||
fn bounding_box(points: &[Point3<f64>]) -> (Point3<f64>, Point3<f64>) {
|
||||
let mut min_pt = points[0];
|
||||
let mut max_pt = points[0];
|
||||
|
||||
for p in points {
|
||||
min_pt.x = min_pt.x.min(p.x);
|
||||
min_pt.y = min_pt.y.min(p.y);
|
||||
min_pt.z = min_pt.z.min(p.z);
|
||||
max_pt.x = max_pt.x.max(p.x);
|
||||
max_pt.y = max_pt.y.max(p.y);
|
||||
max_pt.z = max_pt.z.max(p.z);
|
||||
}
|
||||
|
||||
(min_pt, max_pt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_delaunay_simple() {
|
||||
// Simple cube vertices
|
||||
let points = vec![
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Point3::new(1.0, 0.0, 0.0),
|
||||
Point3::new(0.0, 1.0, 0.0),
|
||||
Point3::new(1.0, 1.0, 0.0),
|
||||
Point3::new(0.0, 0.0, 1.0),
|
||||
Point3::new(1.0, 0.0, 1.0),
|
||||
Point3::new(0.0, 1.0, 1.0),
|
||||
Point3::new(1.0, 1.0, 1.0),
|
||||
];
|
||||
|
||||
let mesh = delaunay_3d(&points).unwrap();
|
||||
|
||||
assert_eq!(mesh.num_vertices(), 8);
|
||||
// A cube should be divided into 5 or 6 tetrahedra
|
||||
assert!(mesh.num_tetrahedra() >= 5);
|
||||
assert!(mesh.num_tetrahedra() <= 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delaunay_random() {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let points: Vec<Point3<f64>> = (0..20)
|
||||
.map(|_| Point3::new(rng.r#gen::<f64>(), rng.r#gen::<f64>(), rng.r#gen::<f64>()))
|
||||
.collect();
|
||||
|
||||
let mesh = delaunay_3d(&points).unwrap();
|
||||
|
||||
assert_eq!(mesh.num_vertices(), 20);
|
||||
assert!(mesh.num_tetrahedra() > 0);
|
||||
|
||||
// All tetrahedra should have positive volume
|
||||
for i in 0..mesh.num_tetrahedra() {
|
||||
let vol = mesh.tetrahedron_volume(i);
|
||||
assert!(vol > 0.0, "Tetrahedron {} has non-positive volume", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_too_few_points() {
|
||||
let points = vec![
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Point3::new(1.0, 0.0, 0.0),
|
||||
Point3::new(0.0, 1.0, 0.0),
|
||||
];
|
||||
|
||||
let result = delaunay_3d(&points);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user