fix: Delaunay3D circumcenter 完整实现 + 测试恢复
This commit is contained in:
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user