From 9bf673e337d0b04fdf9ee22ff29ef6ece37220cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=82=E4=B9=8B=E9=92=B3?= Date: Thu, 23 Jul 2026 23:22:35 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Delaunay3D=20circumcenter=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=AE=9E=E7=8E=B0=20+=20=E6=B5=8B=E8=AF=95=E6=81=A2?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bench/CMakeLists.txt | 73 +++++++++++++++++++++ bench/bench_boolean.cpp | 106 ++++++++++++++++++++++++++++++ bench/bench_bvh.cpp | 90 +++++++++++++++++++++++++ bench/bench_curves.cpp | 77 ++++++++++++++++++++++ bench/bench_delaunay.cpp | 35 ++++++++++ bench/bench_main.cpp | 11 ++++ bench/bench_spatial.cpp | 113 ++++++++++++++++++++++++++++++++ src/mesh/delaunay_3d.cpp | 39 +++++------ tests/mesh/test_delaunay_3d.cpp | 13 ++-- 9 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 bench/CMakeLists.txt create mode 100644 bench/bench_boolean.cpp create mode 100644 bench/bench_bvh.cpp create mode 100644 bench/bench_curves.cpp create mode 100644 bench/bench_delaunay.cpp create mode 100644 bench/bench_main.cpp create mode 100644 bench/bench_spatial.cpp diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt new file mode 100644 index 0000000..bf7c5cb --- /dev/null +++ b/bench/CMakeLists.txt @@ -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) diff --git a/bench/bench_boolean.cpp b/bench/bench_boolean.cpp new file mode 100644 index 0000000..bed42a9 --- /dev/null +++ b/bench/bench_boolean.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 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(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 all_verts; + std::vector> all_tris; + for (size_t fi = 0; fi < sphere_b.num_faces(); ++fi) { + auto fv = sphere_b.face_vertices(static_cast(fi)); + if (fv.size() < 3) continue; + int base = static_cast(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(j), base + static_cast(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); diff --git a/bench/bench_bvh.cpp b/bench/bench_bvh.cpp new file mode 100644 index 0000000..a3adbac --- /dev/null +++ b/bench/bench_bvh.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +using namespace vde; + +namespace { + +/// Generate N pseudo-random triangles inside [-R, R]³ +std::vector random_triangles(int n, double R = 100.0) { + std::mt19937 rng(42); + std::uniform_real_distribution dist(-R, R); + std::vector 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 random_rays(int m, double R = 100.0) { + std::mt19937 rng(42); + std::uniform_real_distribution dist(-R, R); + std::vector 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 rays; + std::vector 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); diff --git a/bench/bench_curves.cpp b/bench/bench_curves.cpp new file mode 100644 index 0000000..1ada647 --- /dev/null +++ b/bench/bench_curves.cpp @@ -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 +#include +#include +#include +#include +#include + +using namespace vde; + +namespace { + +/// A cubic Bezier with control points scattered in [-10, 10]³ +curves::BezierCurve sample_bezier() { + static std::vector 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 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 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 sample_params(int n) { + std::vector t; + t.reserve(n); + for (int i = 0; i < n; ++i) + t.push_back(static_cast(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); diff --git a/bench/bench_delaunay.cpp b/bench/bench_delaunay.cpp new file mode 100644 index 0000000..b46638f --- /dev/null +++ b/bench/bench_delaunay.cpp @@ -0,0 +1,35 @@ +/// bench_delaunay.cpp — 2D Delaunay triangulation benchmarks +/// +/// Metrics: +/// Delaunay2D/N{size} — Bowyer-Watson triangulation time for N random points + +#include +#include +#include +#include + +using namespace vde; + +namespace { + +std::vector random_points_2d(int n, double R = 1000.0) { + std::mt19937 rng(42); + std::uniform_real_distribution dist(-R, R); + std::vector 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); diff --git a/bench/bench_main.cpp b/bench/bench_main.cpp new file mode 100644 index 0000000..b8a532c --- /dev/null +++ b/bench/bench_main.cpp @@ -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_MAIN(); diff --git a/bench/bench_spatial.cpp b/bench/bench_spatial.cpp new file mode 100644 index 0000000..b6f624a --- /dev/null +++ b/bench/bench_spatial.cpp @@ -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 +#include +#include +#include +#include +#include + +using namespace vde; + +namespace { + +std::vector random_points_3d(int n, double R = 100.0) { + std::mt19937 rng(42); + std::uniform_real_distribution dist(-R, R); + std::vector 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 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 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 tree; + std::vector points; + std::vector 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 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 tree; + std::vector points; + std::vector 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); diff --git a/src/mesh/delaunay_3d.cpp b/src/mesh/delaunay_3d.cpp index 5e909e0..36f9d20 100644 --- a/src/mesh/delaunay_3d.cpp +++ b/src/mesh/delaunay_3d.cpp @@ -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 { diff --git a/tests/mesh/test_delaunay_3d.cpp b/tests/mesh/test_delaunay_3d.cpp index a002893..7c6149d 100644 --- a/tests/mesh/test_delaunay_3d.cpp +++ b/tests/mesh/test_delaunay_3d.cpp @@ -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 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 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 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) {