use crate::ids::{FaceId, HalfEdgeId, VertexId};
use crate::storage::MeshStorage;
use crate::traversal::{FaceHalfEdges, VertexAdjacentFaces, VertexAdjacentVerts, VertexRing};
type Vec3 = [f64; 3];
#[inline]
fn sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn add(a: Vec3, b: Vec3) -> Vec3 {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn scale(a: Vec3, s: f64) -> Vec3 {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn length(a: Vec3) -> f64 {
dot(a, a).sqrt()
}
#[inline]
fn normalize(a: Vec3) -> Vec3 {
let l = length(a);
if l < 1e-12 { a } else { scale(a, 1.0 / l) }
}
#[inline]
fn angle_between(u: Vec3, v: Vec3) -> f64 {
let lu = length(u);
let lv = length(v);
if lu < 1e-12 || lv < 1e-12 {
return 0.0;
}
let c = dot(u, v) / (lu * lv);
c.clamp(-1.0, 1.0).acos()
}
pub fn edge_length(mesh: &MeshStorage, he: HalfEdgeId) -> Option<f64> {
let h = mesh.get_halfedge(he)?;
let tip = h.vertex;
let twin_id = h.twin?;
let origin = mesh.get_halfedge(twin_id)?.vertex;
let p_tip = mesh.get_vertex(tip)?.position;
let p_origin = mesh.get_vertex(origin)?.position;
Some(length(sub(p_tip, p_origin)))
}
pub fn face_area(mesh: &MeshStorage, f: FaceId) -> Option<f64> {
let (a, b, c) = face_triangle_positions(mesh, f)?;
Some(0.5 * length(cross(sub(b, a), sub(c, a))))
}
pub fn face_normal(mesh: &MeshStorage, f: FaceId) -> Option<Vec3> {
let (a, b, c) = face_triangle_positions(mesh, f)?;
let n = cross(sub(b, a), sub(c, a));
let l = length(n);
if l < 1e-12 {
return None;
}
Some(scale(n, 1.0 / l))
}
pub fn face_min_angle(mesh: &MeshStorage, f: FaceId) -> Option<f64> {
let (a, b, c) = face_triangle_positions(mesh, f)?;
let angle_a = angle_between(sub(b, a), sub(c, a));
let angle_b = angle_between(sub(a, b), sub(c, b));
let angle_c = angle_between(sub(a, c), sub(b, c));
Some(angle_a.min(angle_b).min(angle_c))
}
pub fn vertex_normal(mesh: &MeshStorage, v: VertexId) -> Option<Vec3> {
let mut accum = [0.0f64; 3];
let mut has_any = false;
for f in VertexAdjacentFaces::new(mesh, v) {
if let (Some(n), Some(area)) = (face_normal(mesh, f), face_area(mesh, f)) {
accum = add(accum, scale(n, area));
has_any = true;
}
}
if !has_any {
return None;
}
let result = normalize(accum);
if length(result) < 1e-12 {
None
} else {
Some(result)
}
}
pub fn cotan_laplacian(mesh: &MeshStorage, v: VertexId) -> Option<[f64; 3]> {
let pos_v = mesh.get_vertex(v)?.position;
let outgoing: Vec<HalfEdgeId> = VertexRing::new(mesh, v).collect();
let mut sum = [0.0; 3];
for &he in &outgoing {
let neighbor = mesh.get_halfedge(he)?.vertex;
let pos_u = mesh.get_vertex(neighbor)?.position;
let weight = cotan_edge_weight(mesh, he)?;
let diff = sub(pos_u, pos_v);
sum = add(sum, scale(diff, weight));
}
Some(sum)
}
pub fn cotan_edge_weight(mesh: &MeshStorage, he: HalfEdgeId) -> Option<f64> {
let h = mesh.get_halfedge(he)?;
let a_pos = mesh.get_vertex(h.vertex)?.position;
let mut weight = 0.0;
if let Some(face) = h.face {
let fhe_ids: Vec<_> = FaceHalfEdges::new(mesh, face).collect();
if fhe_ids.len() == 3 {
let v0 = mesh.get_halfedge(fhe_ids[0])?.vertex;
let v1 = mesh.get_halfedge(fhe_ids[1])?.vertex;
let v2 = mesh.get_halfedge(fhe_ids[2])?.vertex;
let pos = [
mesh.get_vertex(v0)?.position,
mesh.get_vertex(v1)?.position,
mesh.get_vertex(v2)?.position,
];
let src = h.twin.and_then(|t| mesh.get_halfedge(t)).map(|t| t.vertex);
let dst = h.vertex;
if let Some(src_v) = src {
let pos_src_v = mesh.get_vertex(src_v)?.position;
let pos_dst_v = mesh.get_vertex(dst)?.position;
let opp = pos.iter().position(|&p| p != pos_src_v && p != pos_dst_v);
if let Some(idx) = opp {
let opp_pos = pos[idx];
let pos_src = mesh.get_vertex(src_v)?.position;
let pos_dst = a_pos;
let cot = cotan(opp_pos, pos_src, pos_dst);
weight += cot;
}
}
}
}
if let Some(twin) = h.twin
&& let Some(tface) = mesh.get_halfedge(twin)?.face
{
let fhe_ids: Vec<_> = FaceHalfEdges::new(mesh, tface).collect();
if fhe_ids.len() == 3 {
let v0 = mesh.get_halfedge(fhe_ids[0])?.vertex;
let v1 = mesh.get_halfedge(fhe_ids[1])?.vertex;
let v2 = mesh.get_halfedge(fhe_ids[2])?.vertex;
let pos = [
mesh.get_vertex(v0)?.position,
mesh.get_vertex(v1)?.position,
mesh.get_vertex(v2)?.position,
];
let src = h.vertex;
let dst = mesh.get_halfedge(twin)?.vertex;
let pos_src_val = mesh.get_vertex(src)?.position;
let pos_dst_val = mesh.get_vertex(dst)?.position;
let opp = pos
.iter()
.position(|&p| p != pos_src_val && p != pos_dst_val);
if let Some(idx) = opp {
let opp_pos = pos[idx];
let pos_src = mesh.get_vertex(src)?.position;
let pos_dst = mesh.get_vertex(dst)?.position;
let cot = cotan(opp_pos, pos_src, pos_dst);
weight += cot;
}
}
}
Some(weight)
}
fn cotan(opposite: Vec3, a: Vec3, b: Vec3) -> f64 {
let oa = sub(a, opposite);
let ob = sub(b, opposite);
let cross_len = length(cross(oa, ob));
let dot_val = dot(oa, ob);
if cross_len < 1e-14 {
0.0
} else {
dot_val / cross_len
}
}
pub fn dihedral_angle(mesh: &MeshStorage, he: HalfEdgeId) -> Option<f64> {
let h = mesh.get_halfedge(he)?;
let f1 = h.face?;
let twin = h.twin?;
let f2 = mesh.get_halfedge(twin)?.face?;
let n1 = face_normal(mesh, f1)?;
let n2 = face_normal(mesh, f2)?;
let d = dot(n1, n2).clamp(-1.0, 1.0);
Some(d.acos())
}
pub fn is_feature_edge(
mesh: &MeshStorage,
he: HalfEdgeId,
angle_threshold_rad: f64,
) -> Option<bool> {
Some(dihedral_angle(mesh, he)? > angle_threshold_rad)
}
pub fn feature_edges(mesh: &MeshStorage, angle_threshold_rad: f64) -> Vec<HalfEdgeId> {
mesh.halfedge_ids()
.filter(|&he| is_feature_edge(mesh, he, angle_threshold_rad).unwrap_or(false))
.collect()
}
pub fn laplacian_smooth_vertex(mesh: &MeshStorage, v: VertexId) -> Option<Vec3> {
let p = mesh.get_vertex(v)?.position;
let neighbors: Vec<Vec3> = VertexAdjacentVerts::new(mesh, v)
.filter_map(|n| mesh.get_vertex(n).map(|vt| vt.position))
.collect();
if neighbors.is_empty() {
return Some(p);
}
let mut sum = [0.0f64; 3];
for q in &neighbors {
sum = add(sum, *q);
}
Some(scale(sum, 1.0 / neighbors.len() as f64))
}
pub fn laplacian_smooth_mesh(mesh: &mut MeshStorage, lambda: f64, iterations: usize) {
if lambda <= 0.0 || iterations == 0 {
return;
}
let clamped_lambda = lambda.min(1.0);
for _ in 0..iterations {
let new_positions: Vec<(VertexId, Vec3)> = mesh
.vertex_ids()
.filter_map(|v| {
let old_p = mesh.get_vertex(v)?.position;
let target = laplacian_smooth_vertex(mesh, v)?;
let blended = add(
scale(old_p, 1.0 - clamped_lambda),
scale(target, clamped_lambda),
);
Some((v, blended))
})
.collect();
for (v, p) in new_positions {
if let Some(vt) = mesh.get_vertex_mut(v) {
vt.position = p;
}
}
}
}
pub fn point_triangle_distance(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> f64 {
let ab = sub(b, a);
let ac = sub(c, a);
let ap = sub(p, a);
let d1 = dot(ab, ap);
let d2 = dot(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
return length(ap); }
let bp = sub(p, b);
let d3 = dot(ab, bp);
let d4 = dot(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
return length(bp); }
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = if d1 - d3 == 0.0 { 0.0 } else { d1 / (d1 - d3) };
let closest = add(a, scale(ab, v));
return length(sub(p, closest)); }
let cp = sub(p, c);
let d5 = dot(ab, cp);
let d6 = dot(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
return length(cp); }
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = if d2 - d6 == 0.0 { 0.0 } else { d2 / (d2 - d6) };
let closest = add(a, scale(ac, w));
return length(sub(p, closest)); }
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let denom = (d4 - d3) + (d5 - d6);
let w = if denom == 0.0 { 0.0 } else { (d4 - d3) / denom };
let closest = add(b, scale(sub(c, b), w));
return length(sub(p, closest)); }
let denom = va + vb + vc;
if denom.abs() < 1e-20 {
return length(ap).min(length(bp)).min(length(cp));
}
let inv = 1.0 / denom;
let v = vb * inv;
let w = vc * inv;
let closest = add(a, add(scale(ab, v), scale(ac, w)));
length(sub(p, closest))
}
pub fn closest_point_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
let ab = sub(b, a);
let ac = sub(c, a);
let ap = sub(p, a);
let d1 = dot(ab, ap);
let d2 = dot(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
return a; }
let bp = sub(p, b);
let d3 = dot(ab, bp);
let d4 = dot(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
return b; }
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = if d1 - d3 == 0.0 { 0.0 } else { d1 / (d1 - d3) };
return add(a, scale(ab, v)); }
let cp = sub(p, c);
let d5 = dot(ab, cp);
let d6 = dot(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
return c; }
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = if d2 - d6 == 0.0 { 0.0 } else { d2 / (d2 - d6) };
return add(a, scale(ac, w)); }
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let denom = (d4 - d3) + (d5 - d6);
let w = if denom == 0.0 { 0.0 } else { (d4 - d3) / denom };
return add(b, scale(sub(c, b), w)); }
let denom = va + vb + vc;
if denom.abs() < 1e-20 {
let la = dot(ap, ap);
let lb = dot(bp, bp);
let lc = dot(cp, cp);
if la <= lb && la <= lc {
return a;
}
if lb <= lc {
return b;
}
return c;
}
let inv = 1.0 / denom;
let v = vb * inv;
let w = vc * inv;
add(a, add(scale(ab, v), scale(ac, w)))
}
fn face_polygon_positions(mesh: &MeshStorage, f: FaceId) -> Option<Vec<Vec3>> {
let mut verts = Vec::new();
for he in FaceHalfEdges::new(mesh, f) {
let v = mesh.get_halfedge(he)?.vertex;
verts.push(mesh.get_vertex(v)?.position);
}
if verts.is_empty() { None } else { Some(verts) }
}
fn face_triangle_positions(mesh: &MeshStorage, f: FaceId) -> Option<(Vec3, Vec3, Vec3)> {
let verts = face_polygon_positions(mesh, f)?;
if verts.len() != 3 {
return None;
}
Some((verts[0], verts[1], verts[2]))
}
pub fn polygon_area(mesh: &MeshStorage, f: FaceId) -> Option<f64> {
let verts = face_polygon_positions(mesh, f)?;
let n = verts.len();
if n < 3 {
return None;
}
let mut sum = [0.0; 3];
for i in 0..n {
let j = (i + 1) % n;
sum = add(sum, cross(verts[i], verts[j]));
}
Some(0.5 * length(sum))
}
pub fn polygon_normal(mesh: &MeshStorage, f: FaceId) -> Option<Vec3> {
let verts = face_polygon_positions(mesh, f)?;
let n_verts = verts.len();
if n_verts < 3 {
return None;
}
let center = {
let mut c = [0.0; 3];
for v in &verts {
c = add(c, *v);
}
[
c[0] / n_verts as f64,
c[1] / n_verts as f64,
c[2] / n_verts as f64,
]
};
let mut normal = [0.0; 3];
for i in 0..n_verts {
let j = (i + 1) % n_verts;
normal = add(normal, cross(sub(verts[i], center), sub(verts[j], center)));
}
let l = length(normal);
if l < 1e-12 {
None
} else {
Some(scale(normal, 1.0 / l))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AABB {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl AABB {
pub fn new() -> Self {
Self {
min: [f64::MAX; 3],
max: [f64::MIN; 3],
}
}
pub fn from_points(points: &[[f64; 3]]) -> Self {
let mut aabb = Self::new();
for p in points {
aabb.extend(p);
}
aabb
}
pub fn extend(&mut self, point: &[f64; 3]) {
for ((&p, min_i), max_i) in point
.iter()
.zip(self.min.iter_mut())
.zip(self.max.iter_mut())
{
if p < *min_i {
*min_i = p;
}
if p > *max_i {
*max_i = p;
}
}
}
pub fn union(&self, other: &AABB) -> AABB {
let mut result = *self;
for i in 0..3 {
if other.min[i] < result.min[i] {
result.min[i] = other.min[i];
}
if other.max[i] > result.max[i] {
result.max[i] = other.max[i];
}
}
result
}
pub fn center(&self) -> [f64; 3] {
[
(self.min[0] + self.max[0]) / 2.0,
(self.min[1] + self.max[1]) / 2.0,
(self.min[2] + self.max[2]) / 2.0,
]
}
pub fn diagonal(&self) -> f64 {
let d = [
self.max[0] - self.min[0],
self.max[1] - self.min[1],
self.max[2] - self.min[2],
];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
}
pub fn is_empty(&self) -> bool {
self.min[0] > self.max[0]
}
}
impl Default for AABB {
fn default() -> Self {
Self::new()
}
}
pub fn mesh_aabb(mesh: &MeshStorage) -> Option<AABB> {
let mut aabb = AABB::new();
let mut has_vertex = false;
for v in mesh.vertices() {
aabb.extend(&v.position);
has_vertex = true;
}
if has_vertex { Some(aabb) } else { None }
}
pub fn mesh_centroid(mesh: &MeshStorage) -> Option<[f64; 3]> {
let mut sum = [0.0; 3];
let mut count = 0usize;
for v in mesh.vertices() {
for (s, &p) in sum.iter_mut().zip(v.position.iter()) {
*s += p;
}
count += 1;
}
if count == 0 {
None
} else {
Some([
sum[0] / count as f64,
sum[1] / count as f64,
sum[2] / count as f64,
])
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RayHit {
pub position: [f64; 3],
pub t: f64,
pub face: FaceId,
pub barycentric: (f64, f64),
}
pub fn ray_triangle_intersection(
origin: [f64; 3],
direction: [f64; 3],
v0: [f64; 3],
v1: [f64; 3],
v2: [f64; 3],
) -> Option<(f64, f64, f64)> {
let e1 = sub(v1, v0);
let e2 = sub(v2, v0);
let pvec = cross(direction, e2);
let det = dot(e1, pvec);
if det.abs() < 1e-14 {
return None;
}
let inv_det = 1.0 / det;
let tvec = sub(origin, v0);
let u = dot(tvec, pvec) * inv_det;
if !(0.0..=1.0).contains(&u) {
return None;
}
let qvec = cross(tvec, e1);
let v = dot(direction, qvec) * inv_det;
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = dot(e2, qvec) * inv_det;
if t < 1e-12 {
return None;
}
Some((t, u, v))
}
pub fn ray_mesh_intersection(
origin: [f64; 3],
direction: [f64; 3],
mesh: &MeshStorage,
) -> Option<RayHit> {
let mut best: Option<RayHit> = None;
for f in mesh.face_ids() {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
continue;
}
let v0 = mesh.get_vertex(verts[0])?.position;
let v1 = mesh.get_vertex(verts[1])?.position;
let v2 = mesh.get_vertex(verts[2])?.position;
if let Some((t, u, v)) = ray_triangle_intersection(origin, direction, v0, v1, v2) {
let hit = RayHit {
position: [
origin[0] + t * direction[0],
origin[1] + t * direction[1],
origin[2] + t * direction[2],
],
t,
face: f,
barycentric: (u, v),
};
match best {
Some(ref b) if hit.t < b.t => best = Some(hit),
None => best = Some(hit),
_ => {}
}
}
}
best
}
pub fn ray_mesh_intersects(origin: [f64; 3], direction: [f64; 3], mesh: &MeshStorage) -> bool {
let mut count = 0u32;
for f in mesh.face_ids() {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
continue;
}
let v0 = mesh.get_vertex(verts[0]).unwrap().position;
let v1 = mesh.get_vertex(verts[1]).unwrap().position;
let v2 = mesh.get_vertex(verts[2]).unwrap().position;
if ray_triangle_intersection(origin, direction, v0, v1, v2).is_some() {
count += 1;
}
}
count % 2 == 1
}
pub fn surface_area(mesh: &MeshStorage) -> f64 {
mesh.face_ids().filter_map(|f| face_area(mesh, f)).sum()
}
pub fn mesh_volume(mesh: &MeshStorage) -> f64 {
let mut volume = 0.0;
for f in mesh.face_ids() {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
continue;
}
let v0 = mesh.get_vertex(verts[0]).unwrap().position;
let v1 = mesh.get_vertex(verts[1]).unwrap().position;
let v2 = mesh.get_vertex(verts[2]).unwrap().position;
let tetra = v0[0] * (v1[1] * v2[2] - v1[2] * v2[1])
+ v1[0] * (v2[1] * v0[2] - v2[2] * v0[1])
+ v2[0] * (v0[1] * v1[2] - v0[2] * v1[1]);
volume += tetra;
}
volume / 6.0
}
#[derive(Debug, Clone, Copy)]
pub struct VertexCurvature {
pub gaussian: f64,
pub mean: f64,
pub k1: f64,
pub k2: f64,
}
fn mixed_area_at_vertex(mesh: &MeshStorage, v: VertexId) -> f64 {
let mut area = 0.0;
for he in VertexRing::new(mesh, v) {
let h = mesh.get_halfedge(he).unwrap();
let a = h.vertex; let b = h.twin.and_then(|t| mesh.get_halfedge(t)).map(|t| t.vertex); let Some(b) = b else { continue };
let pa = mesh.get_vertex(a).unwrap().position;
let pb = mesh.get_vertex(b).unwrap().position;
let pv = mesh.get_vertex(v).unwrap().position;
let a2 = (pa[0] - pb[0]).powi(2) + (pa[1] - pb[1]).powi(2) + (pa[2] - pb[2]).powi(2);
let b2 = (pv[0] - pa[0]).powi(2) + (pv[1] - pa[1]).powi(2) + (pv[2] - pa[2]).powi(2);
let c2 = (pb[0] - pv[0]).powi(2) + (pb[1] - pv[1]).powi(2) + (pb[2] - pv[2]).powi(2);
let obtuse_at_v = b2 + c2 < a2;
let obtuse_at_a = a2 + b2 < c2;
let obtuse_at_b = a2 + c2 < b2;
if obtuse_at_v {
let tri_area = face_area_from_points(pv, pa, pb);
area += tri_area / 2.0;
} else if obtuse_at_a || obtuse_at_b {
let tri_area = face_area_from_points(pv, pa, pb);
area += tri_area / 4.0;
} else {
let cot_a = cotan_from_pos(pv, pa, pb); let cot_b = cotan_from_pos(pa, pb, pv);
area += (b2 * cot_b + c2 * cot_a) / 8.0; let tri_area = face_area_from_points(pv, pa, pb);
area += tri_area / 3.0;
}
}
if area < 1e-14 { 1e-14 } else { area }
}
fn face_area_from_points(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
0.5 * (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt()
}
fn cotan_from_pos(o: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
let oa = [a[0] - o[0], a[1] - o[1], a[2] - o[2]];
let ob = [b[0] - o[0], b[1] - o[1], b[2] - o[2]];
let dot = oa[0] * ob[0] + oa[1] * ob[1] + oa[2] * ob[2];
let cross = [
oa[1] * ob[2] - oa[2] * ob[1],
oa[2] * ob[0] - oa[0] * ob[2],
oa[0] * ob[1] - oa[1] * ob[0],
];
let cross_len = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
if cross_len < 1e-14 {
0.0
} else {
dot / cross_len
}
}
pub fn gaussian_curvature(mesh: &MeshStorage, v: VertexId) -> Option<f64> {
if !mesh.contains_vertex(v) {
return None;
}
if crate::traversal::is_boundary_vertex(mesh, v) {
return Some(0.0);
}
let mut angle_sum = 0.0;
for he in VertexRing::new(mesh, v) {
let h = mesh.get_halfedge(he)?;
let a = h.vertex;
let b = h.twin.and_then(|t| mesh.get_halfedge(t))?.vertex;
let pv = mesh.get_vertex(v)?.position;
let pa = mesh.get_vertex(a)?.position;
let pb = mesh.get_vertex(b)?.position;
let oa = [pa[0] - pv[0], pa[1] - pv[1], pa[2] - pv[2]];
let ob = [pb[0] - pv[0], pb[1] - pv[1], pb[2] - pv[2]];
let dot = oa[0] * ob[0] + oa[1] * ob[1] + oa[2] * ob[2];
let oa_len = (oa[0] * oa[0] + oa[1] * oa[1] + oa[2] * oa[2]).sqrt();
let ob_len = (ob[0] * ob[0] + ob[1] * ob[1] + ob[2] * ob[2]).sqrt();
if oa_len < 1e-14 || ob_len < 1e-14 {
continue;
}
let cos = (dot / (oa_len * ob_len)).clamp(-1.0, 1.0);
angle_sum += cos.acos();
}
let area = mixed_area_at_vertex(mesh, v);
Some((std::f64::consts::TAU - angle_sum) / area)
}
pub fn mean_curvature(mesh: &MeshStorage, v: VertexId) -> Option<f64> {
if !mesh.contains_vertex(v) {
return None;
}
if crate::traversal::is_boundary_vertex(mesh, v) {
return Some(0.0);
}
let laplacian = cotan_laplacian(mesh, v)?;
let len =
(laplacian[0] * laplacian[0] + laplacian[1] * laplacian[1] + laplacian[2] * laplacian[2])
.sqrt();
if len < 1e-14 {
return Some(0.0);
}
let area = mixed_area_at_vertex(mesh, v);
Some(0.5 * len / area)
}
pub fn principal_curvatures(mesh: &MeshStorage, v: VertexId) -> Option<(f64, f64)> {
let g = gaussian_curvature(mesh, v)?;
let h = mean_curvature(mesh, v)?;
let disc = h * h - g;
if disc < 0.0 {
Some((h, h))
} else {
let sqrt_disc = disc.sqrt();
Some((h + sqrt_disc, h - sqrt_disc))
}
}
pub fn vertex_curvature(mesh: &MeshStorage, v: VertexId) -> Option<VertexCurvature> {
let g = gaussian_curvature(mesh, v)?;
let h = mean_curvature(mesh, v)?;
let (k1, k2) = principal_curvatures(mesh, v)?;
Some(VertexCurvature {
gaussian: g,
mean: h,
k1,
k2,
})
}
pub fn surface_area_par(mesh: &MeshStorage) -> f64 {
use rayon::prelude::*;
let face_ids: Vec<_> = mesh.face_ids().collect();
face_ids
.par_iter()
.filter_map(|&f| face_area(mesh, f))
.sum()
}
pub fn mesh_volume_par(mesh: &MeshStorage) -> f64 {
use rayon::prelude::*;
let face_ids: Vec<_> = mesh.face_ids().collect();
face_ids
.par_iter()
.map(|&f| {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
return 0.0;
}
let v0 = mesh.get_vertex(verts[0]).unwrap().position;
let v1 = mesh.get_vertex(verts[1]).unwrap().position;
let v2 = mesh.get_vertex(verts[2]).unwrap().position;
v0[0] * (v1[1] * v2[2] - v1[2] * v2[1])
+ v1[0] * (v2[1] * v0[2] - v2[2] * v0[1])
+ v2[0] * (v0[1] * v1[2] - v0[2] * v1[1])
})
.sum::<f64>()
/ 6.0
}
pub fn vertex_normals_par(mesh: &MeshStorage) -> Vec<[f64; 3]> {
use rayon::prelude::*;
let verts: Vec<VertexId> = mesh.vertex_ids().collect();
verts
.par_iter()
.map(|&v| vertex_normal(mesh, v).unwrap_or([0.0, 0.0, 0.0]))
.collect()
}
pub fn all_gaussian_curvatures_par(mesh: &MeshStorage) -> Vec<Option<f64>> {
use rayon::prelude::*;
let verts: Vec<VertexId> = mesh.vertex_ids().collect();
verts
.par_iter()
.map(|&v| gaussian_curvature(mesh, v))
.collect()
}
pub fn all_mean_curvatures_par(mesh: &MeshStorage) -> Vec<Option<f64>> {
use rayon::prelude::*;
let verts: Vec<VertexId> = mesh.vertex_ids().collect();
verts.par_iter().map(|&v| mean_curvature(mesh, v)).collect()
}
pub fn laplacian_smooth_mesh_par(mesh: &mut MeshStorage, lambda: f64, iterations: usize) {
use rayon::prelude::*;
if lambda <= 0.0 || iterations == 0 {
return;
}
let clamped_lambda = lambda.min(1.0);
for _ in 0..iterations {
let verts: Vec<VertexId> = mesh.vertex_ids().collect();
let new_positions: Vec<(VertexId, Vec3)> = verts
.par_iter()
.filter_map(|&v| {
let old_p = mesh.get_vertex(v)?.position;
let target = laplacian_smooth_vertex(mesh, v)?;
let blended = add(
scale(old_p, 1.0 - clamped_lambda),
scale(target, clamped_lambda),
);
Some((v, blended))
})
.collect();
for (v, p) in new_positions {
if let Some(vt) = mesh.get_vertex_mut(v) {
vt.position = p;
}
}
}
}
pub fn feature_edges_par(mesh: &MeshStorage, angle_threshold: f64) -> Vec<HalfEdgeId> {
use rayon::prelude::*;
let hes: Vec<HalfEdgeId> = mesh.halfedge_ids().collect();
hes.par_iter()
.filter(|&&he| is_feature_edge(mesh, he, angle_threshold).unwrap_or(false))
.copied()
.collect()
}
pub fn ray_mesh_intersection_par(
mesh: &MeshStorage,
origin: [f64; 3],
direction: [f64; 3],
) -> Vec<RayHit> {
use rayon::prelude::*;
let faces: Vec<FaceId> = mesh.face_ids().collect();
faces
.par_iter()
.filter_map(|&f| {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
return None;
}
let v0 = mesh.get_vertex(verts[0])?.position;
let v1 = mesh.get_vertex(verts[1])?.position;
let v2 = mesh.get_vertex(verts[2])?.position;
let (t, u, v) = ray_triangle_intersection(origin, direction, v0, v1, v2)?;
let position = [
origin[0] + direction[0] * t,
origin[1] + direction[1] * t,
origin[2] + direction[2] * t,
];
Some(RayHit {
position,
t,
face: f,
barycentric: (u, v),
})
})
.collect()
}
pub fn face_aspect_ratio(mesh: &MeshStorage, f: FaceId) -> Option<f64> {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
return None;
}
let p0 = mesh.get_vertex(verts[0])?.position;
let p1 = mesh.get_vertex(verts[1])?.position;
let p2 = mesh.get_vertex(verts[2])?.position;
let l0 = (p1[0] - p0[0]).powi(2) + (p1[1] - p0[1]).powi(2) + (p1[2] - p0[2]).powi(2);
let l1 = (p2[0] - p1[0]).powi(2) + (p2[1] - p1[1]).powi(2) + (p2[2] - p1[2]).powi(2);
let l2 = (p0[0] - p2[0]).powi(2) + (p0[1] - p2[1]).powi(2) + (p0[2] - p2[2]).powi(2);
let l0 = l0.sqrt();
let l1 = l1.sqrt();
let l2 = l2.sqrt();
let l_min = l0.min(l1).min(l2);
let l_max = l0.max(l1).max(l2);
if l_min < 1e-20 {
return None;
}
Some(l_max / l_min)
}
pub fn face_radius_ratio(mesh: &MeshStorage, f: FaceId) -> Option<f64> {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
return None;
}
let p0 = mesh.get_vertex(verts[0])?.position;
let p1 = mesh.get_vertex(verts[1])?.position;
let p2 = mesh.get_vertex(verts[2])?.position;
let a = ((p1[0] - p0[0]).powi(2) + (p1[1] - p0[1]).powi(2) + (p1[2] - p0[2]).powi(2)).sqrt();
let b = ((p2[0] - p1[0]).powi(2) + (p2[1] - p1[1]).powi(2) + (p2[2] - p1[2]).powi(2)).sqrt();
let c = ((p0[0] - p2[0]).powi(2) + (p0[1] - p2[1]).powi(2) + (p0[2] - p2[2]).powi(2)).sqrt();
if a < 1e-20 || b < 1e-20 || c < 1e-20 {
return None;
}
let s = (a + b + c) * 0.5;
let area2 = s * (s - a) * (s - b) * (s - c);
if area2 <= 0.0 {
return Some(0.0);
}
Some(8.0 * (s - a) * (s - b) * (s - c) / (a * b * c))
}
pub fn edge_length_stats(mesh: &MeshStorage) -> EdgeLengthStats {
let mut lengths: Vec<f64> = Vec::with_capacity(mesh.edge_count());
for e in mesh.edge_ids() {
let he = e.halfedge();
if let Some(len) = edge_length(mesh, he) {
lengths.push(len);
}
}
if lengths.is_empty() {
return EdgeLengthStats::default();
}
let n = lengths.len() as f64;
let min = lengths.iter().cloned().fold(f64::INFINITY, f64::min);
let max = lengths.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let mean = lengths.iter().sum::<f64>() / n;
let variance = lengths.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
EdgeLengthStats {
min,
max,
mean,
variance,
count: lengths.len(),
}
}
#[derive(Debug, Clone, Default)]
pub struct EdgeLengthStats {
pub min: f64,
pub max: f64,
pub mean: f64,
pub variance: f64,
pub count: usize,
}
impl EdgeLengthStats {
pub fn std_dev(&self) -> f64 {
self.variance.sqrt()
}
pub fn ratio(&self) -> f64 {
if self.min < 1e-20 {
f64::INFINITY
} else {
self.max / self.min
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MeshQualityStats {
pub aspect_min: f64,
pub aspect_max: f64,
pub aspect_mean: f64,
pub radius_ratio_min: f64,
pub radius_ratio_mean: f64,
pub edges: EdgeLengthStats,
pub face_count: usize,
}
pub fn mesh_quality(mesh: &MeshStorage) -> MeshQualityStats {
let mut aspects: Vec<f64> = Vec::with_capacity(mesh.face_count());
let mut rrs: Vec<f64> = Vec::with_capacity(mesh.face_count());
for f in mesh.face_ids() {
if let Some(ar) = face_aspect_ratio(mesh, f) {
aspects.push(ar);
}
if let Some(rr) = face_radius_ratio(mesh, f) {
rrs.push(rr);
}
}
let face_count = aspects.len();
let aspect_min = aspects.iter().cloned().fold(f64::INFINITY, f64::min);
let aspect_max = aspects.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let aspect_mean = if aspects.is_empty() {
0.0
} else {
aspects.iter().sum::<f64>() / aspects.len() as f64
};
let radius_ratio_min = rrs.iter().cloned().fold(f64::INFINITY, f64::min);
let radius_ratio_mean = if rrs.is_empty() {
0.0
} else {
rrs.iter().sum::<f64>() / rrs.len() as f64
};
MeshQualityStats {
aspect_min,
aspect_max,
aspect_mean,
radius_ratio_min,
radius_ratio_mean,
edges: edge_length_stats(mesh),
face_count,
}
}
pub fn taubin_smooth_mesh(mesh: &mut MeshStorage, lambda: f64, mu: f64, iterations: usize) {
if iterations == 0 || lambda <= 0.0 || mu >= 0.0 {
return;
}
let lambda = lambda.min(1.0);
let mu = mu.max(-1.0);
for _ in 0..iterations {
laplacian_step_signed(mesh, lambda);
laplacian_step_signed(mesh, mu);
}
}
fn laplacian_step_signed(mesh: &mut MeshStorage, lambda: f64) {
let new_positions: Vec<(VertexId, Vec3)> = mesh
.vertex_ids()
.filter_map(|v| {
let old_p = mesh.get_vertex(v)?.position;
let target = laplacian_smooth_vertex(mesh, v)?;
let blended = add(scale(old_p, 1.0 - lambda), scale(target, lambda));
Some((v, blended))
})
.collect();
for (v, p) in new_positions {
if let Some(vt) = mesh.get_vertex_mut(v) {
vt.position = p;
}
}
}
pub fn bilateral_smooth_mesh(
mesh: &mut MeshStorage,
sigma_c: f64,
sigma_s: f64,
iterations: usize,
) {
if iterations == 0 || sigma_c <= 0.0 || sigma_s <= 0.0 {
return;
}
let sigma_c2 = 2.0 * sigma_c * sigma_c;
let sigma_s2 = 2.0 * sigma_s * sigma_s;
for _ in 0..iterations {
let normals: std::collections::HashMap<VertexId, Vec3> = mesh
.vertex_ids()
.filter_map(|v| vertex_normal(mesh, v).map(|n| (v, n)))
.collect();
let verts: Vec<VertexId> = mesh.vertex_ids().collect();
let updates: Vec<(VertexId, Vec3)> = verts
.iter()
.filter_map(|&v| {
let p_i = mesh.get_vertex(v)?.position;
let n_i = normals.get(&v).copied().unwrap_or([0.0; 3]);
let mut sum_disp = [0.0; 3];
let mut sum_w = 0.0;
for n in crate::traversal::VertexAdjacentVerts::new(mesh, v) {
if n == v {
continue;
}
let p_j = mesh.get_vertex(n)?.position;
let n_j = normals.get(&n).copied().unwrap_or([0.0; 3]);
let d = sub(p_j, p_i);
let dist = length(d);
if dist < 1e-20 {
continue;
}
let w_c = (-dist * dist / sigma_c2).exp();
let n_diff = sub(n_i, n_j);
let n_dot = n_diff[0].powi(2) + n_diff[1].powi(2) + n_diff[2].powi(2);
let w_s = (-n_dot / sigma_s2).exp();
let w = w_c * w_s;
sum_disp[0] += w * d[0];
sum_disp[1] += w * d[1];
sum_disp[2] += w * d[2];
sum_w += w;
}
if sum_w < 1e-20 {
return None;
}
let delta = scale(sum_disp, 1.0 / sum_w);
Some((v, add(p_i, delta)))
})
.collect();
for (v, p) in updates {
if let Some(vt) = mesh.get_vertex_mut(v) {
vt.position = p;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{Face, HalfEdge, MeshStorage, Vertex};
fn build_unit_triangle() -> (MeshStorage, [VertexId; 3], FaceId) {
let mut mesh = MeshStorage::new();
let a = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0]));
let b = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0]));
let c = mesh.add_vertex(Vertex::new([0.0, 1.0, 0.0]));
let h_ab = mesh.add_halfedge(HalfEdge::new(b)); let h_bc = mesh.add_halfedge(HalfEdge::new(c)); let h_ca = mesh.add_halfedge(HalfEdge::new(a)); let t_ab = mesh.add_halfedge(HalfEdge::new(a)); let t_bc = mesh.add_halfedge(HalfEdge::new(b)); let t_ca = mesh.add_halfedge(HalfEdge::new(c));
let f = mesh.add_face(Face::new());
for (he, twin, next, prev) in [
(h_ab, t_ab, h_bc, h_ca),
(h_bc, t_bc, h_ca, h_ab),
(h_ca, t_ca, h_ab, h_bc),
] {
let h = mesh.get_halfedge_mut(he).unwrap();
h.twin = Some(twin);
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(f);
}
for (t, he) in [(t_ab, h_ab), (t_bc, h_bc), (t_ca, h_ca)] {
mesh.get_halfedge_mut(t).unwrap().twin = Some(he);
}
mesh.get_vertex_mut(a).unwrap().halfedge = Some(h_ab);
mesh.get_vertex_mut(b).unwrap().halfedge = Some(h_bc);
mesh.get_vertex_mut(c).unwrap().halfedge = Some(h_ca);
mesh.get_face_mut(f).unwrap().halfedge = Some(h_ab);
(mesh, [a, b, c], f)
}
#[test]
fn edge_length_basic() {
let (mesh, _v, _f) = build_unit_triangle();
let he_ab = mesh
.halfedge_ids()
.find(|h| {
let h = mesh.get_halfedge(*h).unwrap();
h.vertex == _v[1] && mesh.get_halfedge(h.twin.unwrap()).unwrap().vertex == _v[0]
})
.unwrap();
assert!((edge_length(&mesh, he_ab).unwrap() - 1.0).abs() < 1e-9);
let he_bc = mesh
.halfedge_ids()
.find(|h| {
let h = mesh.get_halfedge(*h).unwrap();
h.vertex == _v[2] && mesh.get_halfedge(h.twin.unwrap()).unwrap().vertex == _v[1]
})
.unwrap();
assert!((edge_length(&mesh, he_bc).unwrap() - 2.0_f64.sqrt()).abs() < 1e-9);
}
#[test]
fn edge_length_invalid_returns_none() {
let (mesh, _v, _f) = build_unit_triangle();
let bad = HalfEdgeId::default();
assert!(edge_length(&mesh, bad).is_none());
}
#[test]
fn face_area_unit_triangle() {
let (mesh, _v, f) = build_unit_triangle();
assert!((face_area(&mesh, f).unwrap() - 0.5).abs() < 1e-9);
}
#[test]
fn face_normal_ccw_points_up() {
let (mesh, _v, f) = build_unit_triangle();
let n = face_normal(&mesh, f).unwrap();
assert!((n[0] - 0.0).abs() < 1e-9);
assert!((n[1] - 0.0).abs() < 1e-9);
assert!((n[2] - 1.0).abs() < 1e-9);
}
#[test]
fn vertex_normal_corner_of_triangle() {
let (mesh, v, _f) = build_unit_triangle();
let n = vertex_normal(&mesh, v[0]).unwrap();
assert!((n[2] - 1.0).abs() < 1e-9);
}
#[test]
fn face_min_angle_unit_triangle() {
let (mesh, _v, f) = build_unit_triangle();
let min_ang = face_min_angle(&mesh, f).unwrap();
assert!((min_ang - std::f64::consts::FRAC_PI_4).abs() < 1e-9);
}
#[test]
fn face_min_angle_equilateral() {
let mut mesh = MeshStorage::new();
let a = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0]));
let b = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0]));
let c = mesh.add_vertex(Vertex::new([0.5, 3.0_f64.sqrt() / 2.0, 0.0]));
let h_ab = mesh.add_halfedge(HalfEdge::new(b));
let h_bc = mesh.add_halfedge(HalfEdge::new(c));
let h_ca = mesh.add_halfedge(HalfEdge::new(a));
let t_ab = mesh.add_halfedge(HalfEdge::new(a));
let t_bc = mesh.add_halfedge(HalfEdge::new(b));
let t_ca = mesh.add_halfedge(HalfEdge::new(c));
let f = mesh.add_face(Face::new());
for (he, twin, next, prev) in [
(h_ab, t_ab, h_bc, h_ca),
(h_bc, t_bc, h_ca, h_ab),
(h_ca, t_ca, h_ab, h_bc),
] {
let h = mesh.get_halfedge_mut(he).unwrap();
h.twin = Some(twin);
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(f);
}
for (t, he) in [(t_ab, h_ab), (t_bc, h_bc), (t_ca, h_ca)] {
mesh.get_halfedge_mut(t).unwrap().twin = Some(he);
}
mesh.get_vertex_mut(a).unwrap().halfedge = Some(h_ab);
mesh.get_vertex_mut(b).unwrap().halfedge = Some(h_bc);
mesh.get_vertex_mut(c).unwrap().halfedge = Some(h_ca);
mesh.get_face_mut(f).unwrap().halfedge = Some(h_ab);
let min_ang = face_min_angle(&mesh, f).unwrap();
assert!((min_ang - std::f64::consts::FRAC_PI_3).abs() < 1e-9);
}
#[test]
fn laplacian_smooth_vertex_isolated_returns_original() {
let mut mesh = MeshStorage::new();
let v = mesh.add_vertex(Vertex::new([1.0, 2.0, 3.0]));
let p = laplacian_smooth_vertex(&mesh, v).unwrap();
assert_eq!(p, [1.0, 2.0, 3.0]);
}
#[test]
fn laplacian_smooth_vertex_two_neighbors() {
let (mesh, v, _f) = build_unit_triangle();
let p = laplacian_smooth_vertex(&mesh, v[0]).unwrap();
assert!((p[0] - 0.5).abs() < 1e-9);
assert!((p[1] - 0.5).abs() < 1e-9);
assert!((p[2] - 0.0).abs() < 1e-9);
}
#[test]
fn laplacian_smooth_mesh_preserves_centroid() {
let (mut mesh, _v, _f) = build_unit_triangle();
let original_centroid = [1.0 / 3.0, 1.0 / 3.0, 0.0];
laplacian_smooth_mesh(&mut mesh, 1.0, 1);
let positions: Vec<_> = mesh
.vertex_ids()
.map(|v| mesh.get_vertex(v).unwrap().position)
.collect();
let new_centroid = [
positions.iter().map(|p| p[0]).sum::<f64>() / positions.len() as f64,
positions.iter().map(|p| p[1]).sum::<f64>() / positions.len() as f64,
positions.iter().map(|p| p[2]).sum::<f64>() / positions.len() as f64,
];
for i in 0..3 {
assert!(
(new_centroid[i] - original_centroid[i]).abs() < 1e-9,
"重心分量 {} 应保留: 实际 {} vs 期望 {}",
i,
new_centroid[i],
original_centroid[i]
);
}
}
#[test]
fn point_triangle_distance_point_on_face() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let p = [1.0 / 3.0, 1.0 / 3.0, 0.0];
assert!(point_triangle_distance(p, a, b, c).abs() < 1e-9);
}
#[test]
fn point_triangle_distance_above_face() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let p = [0.2, 0.2, 1.0];
assert!((point_triangle_distance(p, a, b, c) - 1.0).abs() < 1e-9);
}
#[test]
fn point_triangle_distance_vertex_region() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let p = [-1.0, -1.0, 0.0];
assert!((point_triangle_distance(p, a, b, c) - 2.0_f64.sqrt()).abs() < 1e-9);
}
#[test]
fn point_triangle_distance_edge_region() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let p = [2.0, 0.0, 0.0];
assert!((point_triangle_distance(p, a, b, c) - 1.0).abs() < 1e-9);
}
#[test]
fn degenerate_face_returns_none_for_normal() {
let mut mesh = MeshStorage::new();
let a = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0]));
let b = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0]));
let c = mesh.add_vertex(Vertex::new([2.0, 0.0, 0.0]));
let h_ab = mesh.add_halfedge(HalfEdge::new(b));
let h_bc = mesh.add_halfedge(HalfEdge::new(c));
let h_ca = mesh.add_halfedge(HalfEdge::new(a));
let t_ab = mesh.add_halfedge(HalfEdge::new(a));
let t_bc = mesh.add_halfedge(HalfEdge::new(b));
let t_ca = mesh.add_halfedge(HalfEdge::new(c));
let f = mesh.add_face(Face::new());
for (he, twin, next, prev) in [
(h_ab, t_ab, h_bc, h_ca),
(h_bc, t_bc, h_ca, h_ab),
(h_ca, t_ca, h_ab, h_bc),
] {
let h = mesh.get_halfedge_mut(he).unwrap();
h.twin = Some(twin);
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(f);
}
for (t, he) in [(t_ab, h_ab), (t_bc, h_bc), (t_ca, h_ca)] {
mesh.get_halfedge_mut(t).unwrap().twin = Some(he);
}
mesh.get_vertex_mut(a).unwrap().halfedge = Some(h_ab);
mesh.get_vertex_mut(b).unwrap().halfedge = Some(h_bc);
mesh.get_vertex_mut(c).unwrap().halfedge = Some(h_ca);
mesh.get_face_mut(f).unwrap().halfedge = Some(h_ab);
assert!(face_normal(&mesh, f).is_none());
assert!((face_area(&mesh, f).unwrap() - 0.0).abs() < 1e-12);
assert!(face_min_angle(&mesh, f).unwrap().abs() < 1e-9);
}
#[test]
fn aabb_unit_triangle() {
let (mesh, _v, _f) = build_unit_triangle();
let aabb = mesh_aabb(&mesh).unwrap();
assert!(!aabb.is_empty());
assert!((aabb.min[0] - 0.0).abs() < 1e-12);
assert!((aabb.max[0] - 1.0).abs() < 1e-12);
assert!((aabb.max[1] - 1.0).abs() < 1e-12);
}
#[test]
fn aabb_empty_mesh() {
let mesh = MeshStorage::new();
assert!(mesh_aabb(&mesh).is_none());
}
#[test]
fn aabb_center_and_diagonal() {
let aabb = AABB {
min: [0.0, 0.0, 0.0],
max: [2.0, 2.0, 2.0],
};
assert_eq!(aabb.center(), [1.0, 1.0, 1.0]);
let diag = aabb.diagonal();
assert!((diag - (12.0_f64).sqrt()).abs() < 1e-12);
}
#[test]
fn centroid_basic() {
let (mesh, _v, _f) = build_unit_triangle();
let c = mesh_centroid(&mesh).unwrap();
assert!((c[0] - 1.0 / 3.0).abs() < 1e-12);
assert!((c[1] - 1.0 / 3.0).abs() < 1e-12);
}
#[test]
fn aabb_on_icosphere() {
let mesh = crate::test_util::build_icosphere(1);
let aabb = mesh_aabb(&mesh).unwrap();
assert!(aabb.min[0] >= -1.0 && aabb.min[0] <= -0.9);
assert!(aabb.max[0] <= 1.0 && aabb.max[0] >= 0.9);
}
#[test]
fn ray_triangle_hit() {
let v0 = [0.0, 0.0, 0.0];
let v1 = [1.0, 0.0, 0.0];
let v2 = [0.0, 1.0, 0.0];
let hit = ray_triangle_intersection([0.25, 0.25, 1.0], [0.0, 0.0, -1.0], v0, v1, v2);
assert!(hit.is_some());
let (t, u, v) = hit.unwrap();
assert!((t - 1.0).abs() < 1e-10);
assert!((u - 0.25).abs() < 1e-10);
assert!((v - 0.25).abs() < 1e-10);
}
#[test]
fn ray_triangle_miss_parallel() {
let v0 = [0.0, 0.0, 0.0];
let v1 = [1.0, 0.0, 0.0];
let v2 = [0.0, 1.0, 0.0];
assert!(ray_triangle_intersection([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], v0, v1, v2).is_none());
}
#[test]
fn test_ray_mesh_intersects() {
let mesh = crate::test_util::build_icosphere(2);
let hits = ray_mesh_intersects([2.0, 0.0, 0.0], [-1.0, 0.0, 0.0], &mesh);
let _ = hits;
}
#[test]
fn test_ray_mesh_intersection_icosphere() {
let mesh = crate::test_util::build_icosphere(2);
let hit = ray_mesh_intersection([2.0, 0.0, 0.0], [-1.0, 0.0, 0.0], &mesh);
assert!(hit.is_some());
let h = hit.unwrap();
assert!((h.position[0] - 1.0).abs() < 0.1);
assert!(h.t > 0.0 && h.t < 2.0);
}
#[test]
fn test_ray_mesh_intersection_miss() {
let mesh = crate::test_util::build_icosphere(2);
assert!(ray_mesh_intersection([3.0, 0.0, 0.0], [1.0, 0.0, 0.0], &mesh).is_none());
}
#[test]
fn surface_area_icosphere() {
let mesh = crate::test_util::build_icosphere(2);
let area = surface_area(&mesh);
assert!(area > 10.0 && area < 15.0, "表面积应在 4π 附近: {}", area);
}
#[test]
fn volume_icosphere() {
let mesh = crate::test_util::build_icosphere(2);
let vol = mesh_volume(&mesh);
assert!(vol > 3.0 && vol < 5.5, "体积应在 4π/3 附近: {}", vol);
}
#[test]
fn gaussian_curvature_icosphere() {
let mesh = crate::test_util::build_icosphere(2);
let mut found = false;
for v in mesh.vertex_ids() {
if let Some(k) = gaussian_curvature(&mesh, v)
&& k.is_finite()
&& k > 0.0
{
found = true;
break;
}
}
assert!(found, "应有正高斯曲率");
}
#[test]
fn mean_curvature_icosphere() {
let mesh = crate::test_util::build_icosphere(2);
let mut found = false;
for v in mesh.vertex_ids() {
if let Some(h) = mean_curvature(&mesh, v)
&& h.is_finite()
&& h > 0.0
{
found = true;
break;
}
}
assert!(found, "应有正平均曲率");
}
#[test]
fn principal_curvatures_nonzero() {
let mesh = crate::test_util::build_icosphere(2);
let mut found = false;
for v in mesh.vertex_ids() {
if let Some((k1, k2)) = principal_curvatures(&mesh, v)
&& k1.is_finite()
&& k2.is_finite()
&& k1 > 0.0
{
found = true;
break;
}
}
assert!(found, "应有正主曲率");
}
#[test]
fn vertex_curvature_struct() {
let mesh = crate::test_util::build_icosphere(2);
let mut tested = false;
for v in mesh.vertex_ids() {
if let Some(c) = vertex_curvature(&mesh, v)
&& c.k1.is_finite()
&& c.k2.is_finite()
{
assert!(c.k1 >= c.k2 - 1e-10);
tested = true;
break;
}
}
assert!(tested, "至少有一个顶点的有效曲率");
}
#[test]
fn aspect_ratio_equilateral_is_one() {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 3.0_f64.sqrt() / 2.0, 0.0],
];
let faces = vec![[0u32, 1, 2]];
let mesh = crate::io::build_mesh_from_vertices_and_faces(&verts, &faces);
let f = mesh.face_ids().next().unwrap();
let ar = face_aspect_ratio(&mesh, f).expect("等边三角形纵横比");
assert!((ar - 1.0).abs() < 1e-10, "等边纵横比应=1, got {ar}");
}
#[test]
fn aspect_ratio_degenerate_returns_none() {
let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let faces = vec![[0u32, 1, 2]];
let mesh = crate::io::build_mesh_from_vertices_and_faces(&verts, &faces);
let f = mesh.face_ids().next().unwrap();
let ar = face_aspect_ratio(&mesh, f);
assert!(ar.is_some(), "aspect_ratio 即使面积 0 也可计算");
}
#[test]
fn radius_ratio_equilateral_is_one() {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 3.0_f64.sqrt() / 2.0, 0.0],
];
let faces = vec![[0u32, 1, 2]];
let mesh = crate::io::build_mesh_from_vertices_and_faces(&verts, &faces);
let f = mesh.face_ids().next().unwrap();
let rr = face_radius_ratio(&mesh, f).expect("等边半径比");
assert!((rr - 1.0).abs() < 1e-10, "等边半径比应=1, got {rr}");
}
#[test]
fn radius_ratio_degenerate_is_zero() {
let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let faces = vec![[0u32, 1, 2]];
let mesh = crate::io::build_mesh_from_vertices_and_faces(&verts, &faces);
let f = mesh.face_ids().next().unwrap();
let rr = face_radius_ratio(&mesh, f).expect("退化三角形半径比");
assert!(rr.abs() < 1e-10, "退化半径比应=0, got {rr}");
}
#[test]
fn edge_length_stats_icosphere_consistent() {
let mesh = crate::test_util::build_icosphere(1);
let stats = edge_length_stats(&mesh);
assert_eq!(stats.count, mesh.edge_count());
assert!(stats.min > 0.0);
assert!(stats.max < 2.0);
assert!(stats.mean > stats.min);
assert!(stats.mean < stats.max);
assert!(stats.ratio().is_finite());
}
#[test]
fn mesh_quality_icosphere_returns_finite_stats() {
let mesh = crate::test_util::build_icosphere(1);
let q = mesh_quality(&mesh);
assert_eq!(q.face_count, mesh.face_count());
assert!(q.aspect_min >= 1.0, "纵横比最小值 ≥ 1");
assert!(q.aspect_max.is_finite());
assert!(q.radius_ratio_min >= 0.0);
assert!(q.radius_ratio_min <= 1.0);
assert!(q.radius_ratio_mean <= 1.0);
assert!(q.edges.count > 0);
}
#[test]
fn taubin_smooth_preserves_volume_better_than_laplacian() {
let mesh0 = crate::test_util::build_icosphere(1);
let area0 = surface_area(&mesh0);
let mut mesh_lap = crate::test_util::build_icosphere(1);
laplacian_smooth_mesh(&mut mesh_lap, 0.5, 20);
let area_lap = surface_area(&mesh_lap);
let laplacian_shrink = (area0 - area_lap).abs() / area0;
let mut mesh_tau = crate::test_util::build_icosphere(1);
taubin_smooth_mesh(&mut mesh_tau, 0.5, -0.53, 20);
let area_tau = surface_area(&mesh_tau);
let taubin_shrink = (area0 - area_tau).abs() / area0;
assert!(
taubin_shrink < laplacian_shrink,
"Taubin 收缩 ({}) 应小于 Laplacian ({})",
taubin_shrink,
laplacian_shrink
);
}
#[test]
fn bilateral_smooth_runs_on_icosphere() {
let mesh0 = crate::test_util::build_icosphere(1);
let mut mesh = crate::test_util::build_icosphere(1);
let stats = edge_length_stats(&mesh);
bilateral_smooth_mesh(&mut mesh, stats.mean, 0.1, 3);
assert_eq!(mesh.vertex_count(), mesh0.vertex_count());
assert_eq!(mesh.face_count(), mesh0.face_count());
}
#[test]
fn taubin_smooth_zero_iterations_is_noop() {
let mut mesh = crate::test_util::build_icosphere(1);
let p0 = mesh.vertex_ids().next().unwrap();
let pos_before = mesh.get_vertex(p0).unwrap().position;
taubin_smooth_mesh(&mut mesh, 0.5, -0.53, 0);
let pos_after = mesh.get_vertex(p0).unwrap().position;
assert_eq!(pos_before, pos_after);
}
#[test]
fn bilateral_smooth_zero_iterations_is_noop() {
let mut mesh = crate::test_util::build_icosphere(1);
let p0 = mesh.vertex_ids().next().unwrap();
let pos_before = mesh.get_vertex(p0).unwrap().position;
bilateral_smooth_mesh(&mut mesh, 0.1, 0.1, 0);
let pos_after = mesh.get_vertex(p0).unwrap().position;
assert_eq!(pos_before, pos_after);
}
}