fix: Delaunay3D circumcenter 完整实现 + 测试恢复
CI / Build & Test (push) Failing after 37s
CI / Release Build (push) Failing after 43s

This commit is contained in:
茂之钳
2026-07-23 23:22:35 +00:00
parent 61fd0c57ae
commit 9bf673e337
9 changed files with 526 additions and 31 deletions
+73
View File
@@ -0,0 +1,73 @@
# ── Google Benchmark (FetchContent) ──
include(FetchContent)
find_package(benchmark QUIET)
if(NOT benchmark_FOUND)
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
google_benchmark
URL https://github.com/google/benchmark/archive/refs/tags/v1.8.3.tar.gz
)
FetchContent_MakeAvailable(google_benchmark)
endif()
# ── Benchmark helper header ──
# Provides vde::bench::measure_ms() chrono fallback if benchmark not available
# (benchmark:: is still preferred when linked)
# ── main entry ──
add_executable(vde_bench
bench_main.cpp
)
target_link_libraries(vde_bench PRIVATE vde benchmark::benchmark)
# ── BVH ──
add_executable(vde_bench_bvh
bench_bvh.cpp
)
target_link_libraries(vde_bench_bvh PRIVATE vde benchmark::benchmark)
# ── Delaunay ──
add_executable(vde_bench_delaunay
bench_delaunay.cpp
)
target_link_libraries(vde_bench_delaunay PRIVATE vde benchmark::benchmark)
# ── Boolean ──
add_executable(vde_bench_boolean
bench_boolean.cpp
)
target_link_libraries(vde_bench_boolean PRIVATE vde benchmark::benchmark)
# ── Curves ──
add_executable(vde_bench_curves
bench_curves.cpp
)
target_link_libraries(vde_bench_curves PRIVATE vde benchmark::benchmark)
# ── Spatial (R-Tree / KD-Tree) ──
add_executable(vde_bench_spatial
bench_spatial.cpp
)
target_link_libraries(vde_bench_spatial PRIVATE vde benchmark::benchmark)
# ── Convenience target: build all benchmarks ──
add_custom_target(vde_benchmarks
DEPENDS
vde_bench
vde_bench_bvh
vde_bench_delaunay
vde_bench_boolean
vde_bench_curves
vde_bench_spatial
COMMENT "All benchmarks built"
)
# ── CTest integration: register benchmarks with optional args ──
# Run individual benchmarks via: ctest -R bench_
enable_testing()
add_test(NAME bench_bvh COMMAND vde_bench_bvh --benchmark_min_time=0.01)
add_test(NAME bench_delaunay COMMAND vde_bench_delaunay --benchmark_min_time=0.01)
add_test(NAME bench_boolean COMMAND vde_bench_boolean --benchmark_min_time=0.01)
add_test(NAME bench_curves COMMAND vde_bench_curves --benchmark_min_time=0.01)
add_test(NAME bench_spatial COMMAND vde_bench_spatial --benchmark_min_time=0.01)
+106
View File
@@ -0,0 +1,106 @@
/// bench_boolean.cpp — 3D mesh boolean operation benchmarks
///
/// Metrics:
/// MeshBoolean_Union/N{faces} — sphere sphere
/// MeshBoolean_Intersection/N{faces} — sphere ∩ sphere
///
/// Two spheres (radius 1.0, separated by 0.5 along X) are generated via BrepModel,
/// discretized to HalfedgeMesh with controlled segment counts, then passed to
/// mesh_boolean().
#include <benchmark/benchmark.h>
#include <vde/brep/brep.h>
#include <vde/brep/modeling.h>
#include <vde/mesh/mesh_boolean.h>
#include <vde/mesh/halfedge_mesh.h>
#include <vde/core/transform.h>
#include <cmath>
using namespace vde;
namespace {
struct SpherePair {
mesh::HalfedgeMesh a;
mesh::HalfedgeMesh b;
};
/// Create two intersecting spheres (sphere_a at (-0.5, 0, 0), sphere_b at (+0.5, 0, 0))
SpherePair make_sphere_pair(int segments) {
int seg_u = segments;
int seg_v = std::max(segments / 2, 4);
auto sphere_a = brep::make_sphere(1.0, seg_u, seg_v).to_mesh();
auto sphere_b = brep::make_sphere(1.0, seg_u, seg_v).to_mesh();
// Translate sphere_b by +1.0 along X so they overlap by ~0.5
core::Transform3D t;
t.set_translation(core::Vector3D(1.0, 0.0, 0.0));
// Since HalfedgeMesh doesn't have a direct transform, we translate vertices manually
std::vector<core::Point3D> vb_verts;
for (size_t vi = 0; vi < sphere_b.num_vertices(); ++vi) {
auto p = sphere_b.vertex(vi);
vb_verts.push_back(core::Point3D(p.x() + 1.0, p.y(), p.z()));
}
// Rebuild: extract face vertex indices and rebuild mesh
mesh::HalfedgeMesh sphere_b_moved;
for (size_t fi = 0; fi < sphere_b.num_faces(); ++fi) {
auto fv = sphere_b.face_vertices(static_cast<int>(fi));
sphere_b_moved.add_face(fv);
}
// Overwrite vertices (add_face adds them at original positions)
// Actually we need a different approach — just vertex-move the existing mesh
// Best: build directly from triangles
mesh::HalfedgeMesh b_moved;
std::vector<core::Point3D> all_verts;
std::vector<std::array<int, 3>> all_tris;
for (size_t fi = 0; fi < sphere_b.num_faces(); ++fi) {
auto fv = sphere_b.face_vertices(static_cast<int>(fi));
if (fv.size() < 3) continue;
int base = static_cast<int>(all_verts.size());
for (int vi : fv) {
auto p = sphere_b.vertex(vi);
all_verts.push_back(core::Point3D(p.x() + 1.0, p.y(), p.z()));
}
// triangulate if > 3
for (size_t j = 1; j + 1 < fv.size(); ++j)
all_tris.push_back({base, base + static_cast<int>(j), base + static_cast<int>(j + 1)});
}
b_moved.build_from_triangles(all_verts, all_tris);
return {sphere_a, b_moved};
}
} // namespace
class MeshBooleanFixture : public benchmark::Fixture {
public:
void SetUp(const benchmark::State& state) override {
auto pair = make_sphere_pair(state.range(0));
mesh_a = std::move(pair.a);
mesh_b = std::move(pair.b);
}
mesh::HalfedgeMesh mesh_a;
mesh::HalfedgeMesh mesh_b;
};
BENCHMARK_DEFINE_F(MeshBooleanFixture, Union)(benchmark::State& state) {
for (auto _ : state) {
auto result = mesh::mesh_boolean(mesh_a, mesh_b, mesh::BooleanOp::Union);
benchmark::DoNotOptimize(result.num_faces());
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK_REGISTER_F(MeshBooleanFixture, Union)->Arg(100)->Arg(500);
BENCHMARK_DEFINE_F(MeshBooleanFixture, Intersection)(benchmark::State& state) {
for (auto _ : state) {
auto result = mesh::mesh_boolean(mesh_a, mesh_b, mesh::BooleanOp::Intersection);
benchmark::DoNotOptimize(result.num_faces());
}
state.SetItemsProcessed(state.iterations());
}
BENCHMARK_REGISTER_F(MeshBooleanFixture, Intersection)->Arg(100)->Arg(500);
+90
View File
@@ -0,0 +1,90 @@
/// bench_bvh.cpp — BVH construction + ray query benchmarks
///
/// Metrics:
/// BVH_Build/N{size} — SAH construction time for N random triangles
/// BVH_RayQuery/N{size} — M random ray queries against a BVH of N triangles
#include <benchmark/benchmark.h>
#include <vde/spatial/bvh.h>
#include <vde/core/triangle.h>
#include <vde/core/point.h>
#include <random>
#include <vector>
#include <cmath>
using namespace vde;
namespace {
/// Generate N pseudo-random triangles inside [-R, R]³
std::vector<core::Triangle3D> random_triangles(int n, double R = 100.0) {
std::mt19937 rng(42);
std::uniform_real_distribution<double> dist(-R, R);
std::vector<core::Triangle3D> tris;
tris.reserve(n);
for (int i = 0; i < n; ++i) {
core::Point3D v0(dist(rng), dist(rng), dist(rng));
core::Point3D v1(v0.x() + dist(rng) * 0.2, v0.y() + dist(rng) * 0.2, v0.z() + dist(rng) * 0.2);
core::Point3D v2(v0.x() + dist(rng) * 0.2, v0.y() + dist(rng) * 0.2, v0.z() + dist(rng) * 0.2);
tris.emplace_back(v0, v1, v2);
}
return tris;
}
/// Generate M random rays with origin/direction inside [-R, R]³
std::vector<core::Ray3Dd> random_rays(int m, double R = 100.0) {
std::mt19937 rng(42);
std::uniform_real_distribution<double> dist(-R, R);
std::vector<core::Ray3Dd> rays;
rays.reserve(m);
for (int i = 0; i < m; ++i) {
core::Point3D org(dist(rng), dist(rng), dist(rng));
core::Vector3D dir(dist(rng), dist(rng), dist(rng));
rays.emplace_back(org, dir);
}
return rays;
}
} // namespace
// ── BVH Build ─────────────────────────────────────────────────────────
static void BVH_Build(benchmark::State& state) {
auto tris = random_triangles(state.range(0));
for (auto _ : state) {
spatial::BVH bvh;
bvh.build(tris);
benchmark::DoNotOptimize(bvh.size());
}
state.SetItemsProcessed(state.iterations() * state.range(0));
}
BENCHMARK(BVH_Build)->Arg(1000)->Arg(10000)->Arg(100000);
// ── BVH Ray Query ─────────────────────────────────────────────────────
class BVH_RayFixture : public benchmark::Fixture {
public:
void SetUp(const benchmark::State& state) override {
tris = random_triangles(state.range(0));
bvh.build(tris);
rays = random_rays(1000); // fixed query count
}
spatial::BVH bvh;
std::vector<core::Ray3Dd> rays;
std::vector<core::Triangle3D> tris;
};
BENCHMARK_DEFINE_F(BVH_RayFixture, BVH_RayQuery)(benchmark::State& state) {
size_t hit_count = 0;
for (auto _ : state) {
for (const auto& ray : rays) {
auto res = bvh.query_ray_nearest(ray);
hit_count += res.has_value();
}
}
benchmark::DoNotOptimize(hit_count);
state.SetItemsProcessed(state.iterations() * rays.size());
}
BENCHMARK_REGISTER_F(BVH_RayFixture, BVH_RayQuery)
->Arg(1000)->Arg(10000)->Arg(100000);
+77
View File
@@ -0,0 +1,77 @@
/// bench_curves.cpp — Bezier / BSpline evaluation benchmarks
///
/// Metrics:
/// BezierEval/N{iterations} — evaluate() throughput on a cubic Bezier
/// BSplineEval/N{iterations} — evaluate() throughput on a cubic BSpline
#include <benchmark/benchmark.h>
#include <vde/curves/bezier_curve.h>
#include <vde/curves/bspline_curve.h>
#include <vde/core/point.h>
#include <random>
#include <vector>
using namespace vde;
namespace {
/// A cubic Bezier with control points scattered in [-10, 10]³
curves::BezierCurve sample_bezier() {
static std::vector<core::Point3D> cp = {
{0.0, 0.0, 0.0}, {3.0, 5.0, -1.0}, {7.0, -2.0, 4.0}, {10.0, 3.0, 2.0}};
return curves::BezierCurve(cp);
}
/// A cubic uniform BSpline with 7 control points
curves::BSplineCurve sample_bspline() {
static std::vector<core::Point3D> cp = {
{0.0, 0.0, 0.0}, {2.0, 4.0, -1.0}, {5.0, 1.0, 3.0},
{8.0, -1.0, 1.0}, {11.0, 3.0, -2.0}, {13.0, 2.0, 0.0}, {15.0, 0.0, 1.0}};
static std::vector<double> knots = {0,0,0,0, 0.25, 0.5, 0.75, 1,1,1,1};
return curves::BSplineCurve(cp, knots, 3);
}
/// N equally spaced t values in [0, 1]
std::vector<double> sample_params(int n) {
std::vector<double> t;
t.reserve(n);
for (int i = 0; i < n; ++i)
t.push_back(static_cast<double>(i) / (n - 1));
return t;
}
} // namespace
// ── Bezier ────────────────────────────────────────────────────────────
static void BezierEval(benchmark::State& state) {
auto curve = sample_bezier();
auto params = sample_params(state.range(0));
double sum = 0;
for (auto _ : state) {
for (double t : params) {
auto p = curve.evaluate(t);
benchmark::DoNotOptimize(sum += p.x());
}
}
benchmark::DoNotOptimize(sum);
state.SetItemsProcessed(state.iterations() * params.size());
}
BENCHMARK(BezierEval)->Arg(10000)->Arg(100000);
// ── BSpline ───────────────────────────────────────────────────────────
static void BSplineEval(benchmark::State& state) {
auto curve = sample_bspline();
auto params = sample_params(state.range(0));
double sum = 0;
for (auto _ : state) {
for (double t : params) {
auto p = curve.evaluate(t);
benchmark::DoNotOptimize(sum += p.x());
}
}
benchmark::DoNotOptimize(sum);
state.SetItemsProcessed(state.iterations() * params.size());
}
BENCHMARK(BSplineEval)->Arg(10000)->Arg(100000);
+35
View File
@@ -0,0 +1,35 @@
/// bench_delaunay.cpp — 2D Delaunay triangulation benchmarks
///
/// Metrics:
/// Delaunay2D/N{size} — Bowyer-Watson triangulation time for N random points
#include <benchmark/benchmark.h>
#include <vde/mesh/delaunay_2d.h>
#include <random>
#include <vector>
using namespace vde;
namespace {
std::vector<core::Point2D> random_points_2d(int n, double R = 1000.0) {
std::mt19937 rng(42);
std::uniform_real_distribution<double> dist(-R, R);
std::vector<core::Point2D> pts;
pts.reserve(n);
for (int i = 0; i < n; ++i)
pts.emplace_back(dist(rng), dist(rng));
return pts;
}
} // namespace
static void Delaunay2D(benchmark::State& state) {
auto pts = random_points_2d(state.range(0));
for (auto _ : state) {
auto result = mesh::delaunay_2d(pts);
benchmark::DoNotOptimize(result.triangles.size());
}
state.SetItemsProcessed(state.iterations() * state.range(0));
}
BENCHMARK(Delaunay2D)->Arg(100)->Arg(1000)->Arg(10000);
+11
View File
@@ -0,0 +1,11 @@
/// bench_main.cpp — Google Benchmark entry point
/// Usage:
/// ./vde_bench — run all registered benchmarks
/// ./vde_bench_bvh — BVH benchmarks only
/// ./vde_bench_delaunay --benchmark_min_time=0.05
///
/// Each benchmark executable is self-contained (links its own main).
#include <benchmark/benchmark.h>
BENCHMARK_MAIN();
+113
View File
@@ -0,0 +1,113 @@
/// bench_spatial.cpp — R-Tree / KD-Tree build + kNN benchmarks
///
/// Metrics:
/// RTree_Build/N{size} — STR-bulk-build time for N random 3D points
/// RTree_kNN/N{size} — kNN(n=10) queries on a pre-built R-Tree
/// KDTree_Build/N{size} — build time for N random 3D points
/// KDTree_kNN/N{size} — kNN(n=10) queries on a pre-built KD-Tree
#include <benchmark/benchmark.h>
#include <vde/spatial/r_tree.h>
#include <vde/spatial/kd_tree.h>
#include <vde/core/point.h>
#include <random>
#include <vector>
using namespace vde;
namespace {
std::vector<core::Point3D> random_points_3d(int n, double R = 100.0) {
std::mt19937 rng(42);
std::uniform_real_distribution<double> dist(-R, R);
std::vector<core::Point3D> pts;
pts.reserve(n);
for (int i = 0; i < n; ++i)
pts.emplace_back(dist(rng), dist(rng), dist(rng));
return pts;
}
/// 20 random query points for kNN
std::vector<core::Point3D> query_points(int n = 20, double R = 100.0) {
return random_points_3d(n, R);
}
} // namespace
// ── R-Tree ────────────────────────────────────────────────────────────
static void RTree_Build(benchmark::State& state) {
auto pts = random_points_3d(state.range(0));
for (auto _ : state) {
spatial::RTree<core::Point3D> tree;
tree.build(pts);
benchmark::DoNotOptimize(tree.size());
}
state.SetItemsProcessed(state.iterations() * state.range(0));
}
BENCHMARK(RTree_Build)->Arg(1000)->Arg(10000)->Arg(100000);
class RTreeFixture : public benchmark::Fixture {
public:
void SetUp(const benchmark::State& state) override {
points = random_points_3d(state.range(0));
tree.build(points);
queries = query_points();
}
spatial::RTree<core::Point3D> tree;
std::vector<core::Point3D> points;
std::vector<core::Point3D> queries;
};
BENCHMARK_DEFINE_F(RTreeFixture, RTree_kNN)(benchmark::State& state) {
size_t total = 0;
for (auto _ : state) {
for (const auto& q : queries) {
auto result = tree.query_knn(q, 10);
total += result.size();
}
}
benchmark::DoNotOptimize(total);
state.SetItemsProcessed(state.iterations() * queries.size());
}
BENCHMARK_REGISTER_F(RTreeFixture, RTree_kNN)->Arg(1000)->Arg(10000)->Arg(100000);
// ── KD-Tree ──────────────────────────────────────────────────────────
static void KDTree_Build(benchmark::State& state) {
auto pts = random_points_3d(state.range(0));
for (auto _ : state) {
spatial::KDTree<core::Point3D> tree;
tree.build(pts);
benchmark::DoNotOptimize(tree.size());
}
state.SetItemsProcessed(state.iterations() * state.range(0));
}
BENCHMARK(KDTree_Build)->Arg(1000)->Arg(10000)->Arg(100000);
class KDTreeFixture : public benchmark::Fixture {
public:
void SetUp(const benchmark::State& state) override {
points = random_points_3d(state.range(0));
tree.build(points);
queries = query_points();
}
spatial::KDTree<core::Point3D> tree;
std::vector<core::Point3D> points;
std::vector<core::Point3D> queries;
};
BENCHMARK_DEFINE_F(KDTreeFixture, KDTree_kNN)(benchmark::State& state) {
size_t total = 0;
for (auto _ : state) {
for (const auto& q : queries) {
auto result = tree.query_knn(q, 10);
total += result.size();
}
}
benchmark::DoNotOptimize(total);
state.SetItemsProcessed(state.iterations() * queries.size());
}
BENCHMARK_REGISTER_F(KDTreeFixture, KDTree_kNN)->Arg(1000)->Arg(10000)->Arg(100000);
+17 -22
View File
@@ -19,32 +19,27 @@ struct Tetra {
};
Point3D circumcenter_3d(const Point3D& a, const Point3D& b, const Point3D& c, const Point3D& d) {
// Solve the linear system derived from equal-distance constraints.
// |a - C|² = |b - C|² → 2(b - a)·C = |b|² - |a|²
// Same for (c - a) and (d - a), yielding 3 equations in C.
Vector3D ba = b - a, ca = c - a, da = d - a;
double len_ba = ba.squaredNorm(), len_ca = ca.squaredNorm(), len_da = da.squaredNorm();
double a2 = a.squaredNorm();
double R1 = 0.5 * (b.squaredNorm() - a2);
double R2 = 0.5 * (c.squaredNorm() - a2);
double R3 = 0.5 * (d.squaredNorm() - a2);
// Cramer rule for circumcenter
double denominator = 2.0 * (ba.x()*(ca.y()*da.z()-ca.z()*da.y())
- ba.y()*(ca.x()*da.z()-ca.z()*da.x())
+ ba.z()*(ca.x()*da.y()-ca.y()*da.x()));
if (std::abs(denominator) < 1e-15)
// M = [ba ca da] (columns). The system is Mᵀ·C = [R1 R2 R3]ᵀ.
Eigen::Matrix3d M;
M.col(0) = ba; M.col(1) = ca; M.col(2) = da;
double det = M.determinant();
if (std::abs(det) < 1e-15) {
// Coplanar / collinear — circumsphere undefined; fall back to centroid.
return (a + b + c + d) * 0.25;
}
double inv = 1.0 / denominator;
double cx = (len_ba*(ca.y()*da.z()-ca.z()*da.y())
- ca.y()*(len_ca*da.z()-da.y()*len_da)
+ da.y()*(len_ca*ca.z()-ca.y()*len_da) * 0) * inv; // Simplified
// Use barycentric approach
Vector3D cross_cd = ca.cross(da);
double det = ba.dot(cross_cd);
if (std::abs(det) < 1e-15) return (a + b + c + d) * 0.25;
Point3D center;
center.x() = a.x() + (len_ba * cross_cd.x() + ca.norm() * (da.cross(ba)).x()
+ da.norm() * (ba.cross(ca)).x()) / (2.0 * det);
// Simplified — fallback to average
(void)len_da;
return (a + b + c + d) * 0.25;
Eigen::Vector3d rhs(R1, R2, R3);
Eigen::Vector3d C = M.transpose().colPivHouseholderQr().solve(rhs);
return C;
}
struct Face3 {
+4 -9
View File
@@ -78,8 +78,6 @@ TEST(Delaunay3DTest, EmptyInput_ReturnsEmpty) {
}
TEST(Delaunay3DTest, FourPointsTetrahedron_OneTetrahedron) {
// NOTE: 3D Delaunay implementation is WIP — circumcenter formula incomplete.
// Once the Bowyer-Watson insertion is fixed, expect ≥1 tetrahedron.
std::vector<Point3D> pts = {
{1, 1, 1},
{1, -1, -1},
@@ -88,12 +86,10 @@ TEST(Delaunay3DTest, FourPointsTetrahedron_OneTetrahedron) {
};
auto result = delaunay_3d(pts);
EXPECT_EQ(result.vertices.size(), 4u);
// EXPECT_GE(result.tetrahedra.size(), 1u); // TODO: fix circumcenter
EXPECT_GE(result.tetrahedra.size(), 1u);
}
TEST(Delaunay3DTest, FivePointsCube_VerifyDelaunayProperty) {
// 4 corners of a tetrahedron + 1 point inside the circumsphere
// Use a well-distributed set: origin + unit tetrahedron
std::vector<Point3D> pts = {
{0, 0, 0},
{1, 0, 0},
@@ -103,11 +99,10 @@ TEST(Delaunay3DTest, FivePointsCube_VerifyDelaunayProperty) {
};
auto result = delaunay_3d(pts);
EXPECT_EQ(result.vertices.size(), 5u);
// EXPECT_GE(result.tetrahedra.size(), 2u); // TODO: fix circumcenter
EXPECT_GE(result.tetrahedra.size(), 2u);
}
TEST(Delaunay3DTest, FivePointsDelaunayProperty_EmptyCircumsphere) {
// Regular tetrahedron + center point — should satisfy Delaunay
std::vector<Point3D> pts = {
{1, 1, 1},
{1, -1, -1},
@@ -117,8 +112,8 @@ TEST(Delaunay3DTest, FivePointsDelaunayProperty_EmptyCircumsphere) {
};
auto result = delaunay_3d(pts);
EXPECT_EQ(result.vertices.size(), 5u);
// EXPECT_GE(result.tetrahedra.size(), 4u); // TODO: fix circumcenter
(void)result;
EXPECT_GE(result.tetrahedra.size(), 4u);
EXPECT_TRUE(verify_empty_circumsphere(result, pts));
}
TEST(Delaunay3DTest, CollinearPoints_HandlesGracefully) {