feat: 测试覆盖扩展 + 文档更新
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
add_vde_test(test_halfedge)
|
||||
add_vde_test(test_delaunay)
|
||||
add_vde_test(test_quality)
|
||||
add_vde_test(test_smooth)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/mesh/mesh_quality.h"
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
|
||||
using namespace vde::mesh;
|
||||
|
||||
TEST(MeshQualityTest, EquilateralTriangle_PerfectQuality) {
|
||||
HalfedgeMesh mesh;
|
||||
// Equilateral triangle side length ≈ 1.155 for area ≈ 0.577
|
||||
double h = std::sqrt(3.0) / 2.0;
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 0), Point3D(1, 0, 0), Point3D(0.5, h, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
|
||||
// Equilateral triangle: all angles = 60°
|
||||
EXPECT_NEAR(q.min_angle_deg, 60.0, 1.0);
|
||||
EXPECT_NEAR(q.max_angle_deg, 60.0, 1.0);
|
||||
// Aspect ratio ~1 for equilateral
|
||||
EXPECT_NEAR(q.avg_aspect_ratio, 1.0, 0.1);
|
||||
EXPECT_EQ(q.degenerate_faces, 0u);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, DegenerateTriangle_ZeroArea) {
|
||||
HalfedgeMesh mesh;
|
||||
// Collinear points → zero area
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 0), Point3D(1, 0, 0), Point3D(2, 0, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
EXPECT_EQ(q.degenerate_faces, 1u);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, ObtuseTriangle_LargeMaxAngle) {
|
||||
HalfedgeMesh mesh;
|
||||
// Very flat obtuse triangle: (0,0), (10,0), (0,0.1)
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 0), Point3D(10, 0, 0), Point3D(0, 0.1, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
// Max angle should be very close to 180 (or at least > 90)
|
||||
EXPECT_GT(q.max_angle_deg, 90.0);
|
||||
// Min angle should be very small
|
||||
EXPECT_LT(q.min_angle_deg, 10.0);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, RightTriangle) {
|
||||
HalfedgeMesh mesh;
|
||||
// 3-4-5 right triangle
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 0), Point3D(3, 0, 0), Point3D(0, 4, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
// Right triangle: one angle ~90°, others ~36.87° and ~53.13°
|
||||
EXPECT_NEAR(q.max_angle_deg, 90.0, 2.0);
|
||||
EXPECT_GT(q.min_angle_deg, 30.0);
|
||||
EXPECT_EQ(q.degenerate_faces, 0u);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, MultipleTriangles_Mixed) {
|
||||
HalfedgeMesh mesh;
|
||||
// Two triangles: one good, one degenerate
|
||||
double h = std::sqrt(3.0) / 2.0;
|
||||
mesh.build_from_triangles(
|
||||
{
|
||||
Point3D(0, 0, 0), Point3D(1, 0, 0), Point3D(0.5, h, 0), // equilateral
|
||||
Point3D(10, 0, 0), Point3D(11, 0, 0), Point3D(12, 0, 0), // degenerate
|
||||
},
|
||||
{{0, 1, 2}, {3, 4, 5}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
EXPECT_EQ(q.degenerate_faces, 1u);
|
||||
// Average aspect ratio should reflect the mix
|
||||
EXPECT_GT(q.avg_aspect_ratio, 1.0);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, EmptyMesh) {
|
||||
HalfedgeMesh mesh;
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
EXPECT_EQ(q.degenerate_faces, 0u);
|
||||
// Default values for empty mesh
|
||||
EXPECT_DOUBLE_EQ(q.min_angle_deg, 0.0);
|
||||
EXPECT_DOUBLE_EQ(q.max_angle_deg, 0.0);
|
||||
}
|
||||
|
||||
TEST(MeshQualityTest, HighAspectRatioTriangle) {
|
||||
HalfedgeMesh mesh;
|
||||
// Very thin triangle
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 0), Point3D(100, 0, 0), Point3D(0, 0.01, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
auto q = evaluate_mesh_quality(mesh);
|
||||
// Aspect ratio should be very high
|
||||
EXPECT_GT(q.max_aspect_ratio, 10.0);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/mesh/mesh_smooth.h"
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
|
||||
using namespace vde::mesh;
|
||||
|
||||
// Helper: create a simple 4-vertex quad mesh (2 triangles)
|
||||
static HalfedgeMesh make_quad_mesh() {
|
||||
HalfedgeMesh mesh;
|
||||
// Quad with slight noise on one vertex
|
||||
mesh.build_from_triangles(
|
||||
{
|
||||
Point3D(0, 0, 0), Point3D(1, 0, 0),
|
||||
Point3D(1, 1, 0.5), // perturbed in Z
|
||||
Point3D(0, 1, 0),
|
||||
},
|
||||
{{0, 1, 2}, {0, 2, 3}}
|
||||
);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, LaplacianSmooth_ReducesNoise) {
|
||||
HalfedgeMesh mesh = make_quad_mesh();
|
||||
|
||||
// Original: vertex 2 has Z = 0.5 perturbation
|
||||
EXPECT_NEAR(mesh.vertex(2).z(), 0.5, 1e-6);
|
||||
|
||||
SmoothOptions opts;
|
||||
opts.method = SmoothMethod::Laplacian;
|
||||
opts.iterations = 5;
|
||||
opts.lambda = 0.5;
|
||||
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh, opts);
|
||||
|
||||
// After smoothing, the noise should be reduced
|
||||
// Vertex 2 Z should be closer to 0 (the Laplacian average)
|
||||
EXPECT_LT(std::abs(smoothed.vertex(2).z()), 0.5);
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, LaplacianNoop_FlatMesh) {
|
||||
HalfedgeMesh mesh;
|
||||
mesh.build_from_triangles(
|
||||
{
|
||||
Point3D(0, 0, 0), Point3D(1, 0, 0),
|
||||
Point3D(1, 1, 0), Point3D(0, 1, 0),
|
||||
},
|
||||
{{0, 1, 2}, {0, 2, 3}}
|
||||
);
|
||||
|
||||
SmoothOptions opts;
|
||||
opts.method = SmoothMethod::Laplacian;
|
||||
opts.iterations = 3;
|
||||
opts.lambda = 0.5;
|
||||
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh, opts);
|
||||
|
||||
// Flat mesh should remain flat
|
||||
for (size_t i = 0; i < smoothed.num_vertices(); ++i) {
|
||||
EXPECT_NEAR(smoothed.vertex(i).z(), 0.0, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, TaubinPreservesVolume) {
|
||||
HalfedgeMesh mesh = make_quad_mesh();
|
||||
|
||||
// Compute original bounding box volume
|
||||
auto original_bounds = mesh.bounds();
|
||||
double original_volume = original_bounds.volume();
|
||||
|
||||
SmoothOptions opts;
|
||||
opts.method = SmoothMethod::Taubin;
|
||||
opts.lambda = 0.5;
|
||||
opts.mu = -0.53;
|
||||
opts.iterations = 5;
|
||||
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh, opts);
|
||||
auto smoothed_bounds = smoothed.bounds();
|
||||
double smoothed_volume = smoothed_bounds.volume();
|
||||
|
||||
// Taubin should better preserve volume than pure Laplacian
|
||||
// Volume change should be reasonable (not shrinking to 0)
|
||||
EXPECT_GT(smoothed_volume, 0.0);
|
||||
|
||||
// Noise reduction: vertex 2 Z should be smoothed
|
||||
EXPECT_LT(std::abs(smoothed.vertex(2).z()), 0.5);
|
||||
|
||||
// Volume change ratio should be reasonable (< 50% change)
|
||||
double ratio = std::abs(smoothed_volume - original_volume) / std::max(original_volume, 1e-9);
|
||||
EXPECT_LT(ratio, 0.5);
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, DefaultOptions) {
|
||||
HalfedgeMesh mesh = make_quad_mesh();
|
||||
|
||||
// Default options: Laplacian, 10 iterations, lambda=0.5
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh);
|
||||
|
||||
EXPECT_EQ(smoothed.num_vertices(), mesh.num_vertices());
|
||||
EXPECT_EQ(smoothed.num_faces(), mesh.num_faces());
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, DifferentIterationCounts) {
|
||||
HalfedgeMesh mesh = make_quad_mesh();
|
||||
double original_z = mesh.vertex(2).z();
|
||||
|
||||
SmoothOptions opts_few;
|
||||
opts_few.method = SmoothMethod::Laplacian;
|
||||
opts_few.iterations = 1;
|
||||
opts_few.lambda = 0.5;
|
||||
HalfedgeMesh smooth_1 = smooth_mesh(mesh, opts_few);
|
||||
|
||||
SmoothOptions opts_many;
|
||||
opts_many.method = SmoothMethod::Laplacian;
|
||||
opts_many.iterations = 20;
|
||||
opts_many.lambda = 0.5;
|
||||
HalfedgeMesh smooth_20 = smooth_mesh(mesh, opts_many);
|
||||
|
||||
// More iterations → more smoothing (vertex 2 Z closer to 0)
|
||||
double delta_1 = std::abs(smooth_1.vertex(2).z() - 0.0);
|
||||
double delta_20 = std::abs(smooth_20.vertex(2).z() - 0.0);
|
||||
// 20 iterations should smooth more than 1
|
||||
EXPECT_LE(delta_20, delta_1 + 1e-6);
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, SingleTriangleUnaffected) {
|
||||
// Single triangle: smoothing has no effect (all vertices are boundary)
|
||||
HalfedgeMesh mesh;
|
||||
mesh.build_from_triangles(
|
||||
{Point3D(0, 0, 2), Point3D(1, 0, 0), Point3D(0, 1, 0)},
|
||||
{{0, 1, 2}}
|
||||
);
|
||||
|
||||
SmoothOptions opts;
|
||||
opts.method = SmoothMethod::Laplacian;
|
||||
opts.iterations = 5;
|
||||
opts.lambda = 0.5;
|
||||
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh, opts);
|
||||
|
||||
// Single triangle: topological boundary, so minimal change expected
|
||||
// But implementation might still move vertices
|
||||
EXPECT_EQ(smoothed.num_vertices(), 3u);
|
||||
EXPECT_EQ(smoothed.num_faces(), 1u);
|
||||
}
|
||||
|
||||
TEST(MeshSmoothTest, ZeroIterations) {
|
||||
HalfedgeMesh mesh = make_quad_mesh();
|
||||
|
||||
SmoothOptions opts;
|
||||
opts.iterations = 0;
|
||||
|
||||
HalfedgeMesh smoothed = smooth_mesh(mesh, opts);
|
||||
|
||||
// Zero iterations → mesh unchanged
|
||||
for (size_t i = 0; i < mesh.num_vertices(); ++i) {
|
||||
EXPECT_NEAR((smoothed.vertex(i) - mesh.vertex(i)).norm(), 0.0, 1e-9);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user