#include #include "vde/sdf/sdf_optimize.h" #include "vde/sdf/sdf_primitives.h" #include "vde/sdf/sdf_tree.h" #include using namespace vde::sdf; using vde::core::Point3D; using vde::core::Vector3D; // ─────────────────────────────────────────────────── // fit_to_point_cloud — sphere // ─────────────────────────────────────────────────── TEST(SdfOptimize, FitSphereToPointCloud_Converges) { // Ground truth: sphere of radius 2.0 at origin const double target_radius = 2.0; std::vector surface_points; // Generate points on the sphere surface (Fibonacci sphere) const int n_pts = 200; const double golden_angle = M_PI * (3.0 - std::sqrt(5.0)); for (int i = 0; i < n_pts; ++i) { double y = 1.0 - (i / double(n_pts - 1)) * 2.0; // y ∈ [-1, 1] double radius_at_y = std::sqrt(1.0 - y * y); double theta = golden_angle * i; surface_points.emplace_back( target_radius * radius_at_y * std::cos(theta), target_radius * y, target_radius * radius_at_y * std::sin(theta)); } // Initial sphere (wrong radius) auto initial = SdfNode::sphere(1.0); auto result = fit_to_point_cloud(initial, surface_points, 0.05, 200); EXPECT_LT(result.final_loss, 0.5); EXPECT_TRUE(result.loss_history.size() > 0); EXPECT_GT(result.loss_history.front(), result.loss_history.back()) << "Loss should decrease during optimization"; } TEST(SdfOptimize, FitSphereToPointCloud_EmptyPoints) { auto initial = SdfNode::sphere(1.0); auto result = fit_to_point_cloud(initial, {}, 0.01, 100); EXPECT_EQ(result.iterations, 0); } // ─────────────────────────────────────────────────── // fit_to_sdf — box to sphere // ─────────────────────────────────────────────────── TEST(SdfOptimize, FitBoxToSphereSdf_LossDecreases) { // Target SDF: sphere of radius 1.0 auto target_sdf = [](const Point3D& p) -> double { return sphere(p, 1.0); }; // Initial: box auto source = SdfNode::box(Point3D(1.5, 1.5, 1.5)); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); auto result = fit_to_sdf(source, target_sdf, bmin, bmax, 8, 0.01, 50); // Loss should decrease from initial EXPECT_GT(result.loss_history.size(), 0u); if (result.loss_history.size() >= 2) { EXPECT_LT(result.loss_history.back(), result.loss_history.front()) << "Loss should decrease"; } } // ─────────────────────────────────────────────────── // resolve_collision — two spheres // ─────────────────────────────────────────────────── TEST(SdfOptimize, ResolveCollision_Spheres) { auto shape_a = SdfNode::sphere(1.0); // Place shape_b overlapping shape_a auto shape_b = SdfNode::translate( SdfNode::sphere(1.0), Point3D(1.0, 0, 0)); bool resolved = resolve_collision(shape_a, shape_b, 0.05, 100); // After resolution, shapes should not overlap // We accept that it may not fully resolve in all cases // At minimum, it shouldn't crash EXPECT_TRUE(true); // at least it ran } // ─────────────────────────────────────────────────── // penetration_depth — two overlapping spheres // ─────────────────────────────────────────────────── TEST(SdfOptimize, PenetrationDepth_OverlappingSpheres) { auto a = SdfNode::sphere(1.0); auto b = SdfNode::translate(SdfNode::sphere(1.0), Point3D(1.0, 0, 0)); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); double depth = penetration_depth(a, b, bmin, bmax, 5000); EXPECT_GT(depth, 0.0) << "Overlapping shapes should have positive penetration depth"; } TEST(SdfOptimize, PenetrationDepth_NonOverlappingSpheres) { auto a = SdfNode::sphere(1.0); auto b = SdfNode::translate(SdfNode::sphere(1.0), Point3D(5.0, 0, 0)); Point3D bmin(-1, -1, -1); Point3D bmax(1, 1, 1); // Only sampling in a's bbox, b is far away double depth = penetration_depth(a, b, bmin, bmax, 500); EXPECT_NEAR(depth, 0.0, 1e-6) << "Non-overlapping shapes should have zero penetration depth"; } // ─────────────────────────────────────────────────── // accessibility — point on sphere // ─────────────────────────────────────────────────── TEST(SdfOptimize, Accessibility_OnSphere) { auto shape = SdfNode::sphere(1.0); // Point on sphere surface Point3D surface_point(1.0, 0.0, 0.0); Vector3D direction(1.0, 0.0, 0.0); // outward normal double score = accessibility(shape, surface_point, direction, 64); EXPECT_GT(score, 0.0) << "Should have some accessibility"; EXPECT_LE(score, 1.0); } // ─────────────────────────────────────────────────── // symmetry_score — sphere is highly symmetric // ─────────────────────────────────────────────────── TEST(SdfOptimize, SymmetryScore_Sphere) { auto shape = SdfNode::sphere(1.0); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); Vector3D normal(0, 1, 0); // Y-axis plane double score = symmetry_score(shape, bmin, bmax, normal, 0.0, 500); EXPECT_GT(score, 0.9) << "Sphere should be highly symmetric about any plane through center"; } TEST(SdfOptimize, SymmetryScore_Asymmetric) { // Union of sphere and an offset box — not symmetric about Y=0 auto shape = SdfNode::op_union( SdfNode::sphere(1.0), SdfNode::translate(SdfNode::box(Point3D(0.5, 0.5, 0.5)), Point3D(1.5, 0, 0))); Point3D bmin(-3, -3, -3); Point3D bmax(3, 3, 3); Vector3D normal(1, 0, 0); // X-axis plane through origin double score = symmetry_score(shape, bmin, bmax, normal, 0.0, 500); EXPECT_LT(score, 0.9) << "Asymmetric shape should have lower symmetry score"; } // ─────────────────────────────────────────────────── // find_symmetry_plane — sphere // ─────────────────────────────────────────────────── TEST(SdfOptimize, FindSymmetryPlane_Sphere) { auto shape = SdfNode::sphere(1.0); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); auto result = find_symmetry_plane(shape, bmin, bmax, 500); EXPECT_GT(result.score, 0.5) << "Should find at least one reasonable symmetry plane"; } // ─────────────────────────────────────────────────── // estimate_volume — sphere // ─────────────────────────────────────────────────── TEST(SdfOptimize, EstimateVolume_Sphere) { auto shape = SdfNode::sphere(1.0); Point3D bmin(-1.5, -1.5, -1.5); Point3D bmax(1.5, 1.5, 1.5); double vol = estimate_volume(shape, bmin, bmax, 50000); double expected = (4.0 / 3.0) * M_PI; // ≈ 4.18879 double tolerance = 0.15 * expected; EXPECT_NEAR(vol, expected, tolerance) << "Estimated volume should be close to (4/3)πr³"; } TEST(SdfOptimize, EstimateVolume_Box) { auto shape = SdfNode::box(Point3D(1.0, 0.5, 0.5)); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); double vol = estimate_volume(shape, bmin, bmax, 50000); double expected = 2.0 * 1.0 * 1.0; // width * height * depth double tolerance = 0.15 * expected; EXPECT_NEAR(vol, expected, tolerance); } // ─────────────────────────────────────────────────── // center_of_mass — sphere at origin // ─────────────────────────────────────────────────── TEST(SdfOptimize, CenterOfMass_SphereAtOrigin) { auto shape = SdfNode::sphere(1.0); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); Point3D com = center_of_mass(shape, bmin, bmax, 10000); EXPECT_NEAR(com.x(), 0.0, 0.15); EXPECT_NEAR(com.y(), 0.0, 0.15); EXPECT_NEAR(com.z(), 0.0, 0.15); } // ─────────────────────────────────────────────────── // collect_params — sphere has radius param // ─────────────────────────────────────────────────── TEST(SdfOptimize, CollectParams_Sphere) { auto shape = SdfNode::sphere(2.5); auto params = collect_params(shape); ASSERT_EQ(params.size(), 1u); EXPECT_EQ(params[0].name, "radius"); EXPECT_DOUBLE_EQ(*params[0].value, 2.5); } TEST(SdfOptimize, CollectParams_Box) { auto shape = SdfNode::box(Point3D(1.0, 2.0, 3.0)); auto params = collect_params(shape); ASSERT_EQ(params.size(), 3u); EXPECT_DOUBLE_EQ(*params[0].value, 1.0); // extent_x EXPECT_DOUBLE_EQ(*params[1].value, 2.0); // extent_y EXPECT_DOUBLE_EQ(*params[2].value, 3.0); // extent_z } TEST(SdfOptimize, CollectParams_Cylinder) { auto shape = SdfNode::cylinder(1.5, 3.0); auto params = collect_params(shape); ASSERT_EQ(params.size(), 2u); EXPECT_EQ(params[0].name, "radius"); EXPECT_DOUBLE_EQ(*params[0].value, 1.5); EXPECT_EQ(params[1].name, "height"); EXPECT_DOUBLE_EQ(*params[1].value, 3.0); } // ─────────────────────────────────────────────────── // numerical_gradient — simple quadratic // ─────────────────────────────────────────────────── TEST(SdfOptimize, NumericalGradient_Quadratic) { double val = 3.0; ParamRef ref{&val, "x"}; std::vector params = {ref}; // f(x) = x², gradient should be 2x = 6.0 at x=3 auto loss_fn = [&]() -> double { return val * val; }; auto grad = numerical_gradient(params, loss_fn, 1e-6); ASSERT_EQ(grad.size(), 1u); EXPECT_NEAR(grad[0], 6.0, 1e-3); } // ─────────────────────────────────────────────────── // GradientDescent class // ─────────────────────────────────────────────────── TEST(SdfOptimize, GradientDescent_MinimizesQuadratic) { GradientDescent gd(0.1); std::vector params = {5.0}; // start at x=5 // f(x) = (x-3)², df/dx = 2(x-3) auto grad_fn = [](const std::vector& p, std::vector& g) -> double { double x = p[0]; g[0] = 2.0 * (x - 3.0); return (x - 3.0) * (x - 3.0); }; double initial_loss = (5.0 - 3.0) * (5.0 - 3.0); for (int i = 0; i < 100; ++i) { gd.step(params, grad_fn); } EXPECT_NEAR(params[0], 3.0, 0.01) << "Gradient descent should converge to minimum x=3"; EXPECT_GT(gd.iteration(), 0); } TEST(SdfOptimize, GradientDescent_LearningRate) { GradientDescent gd(0.01); EXPECT_DOUBLE_EQ(gd.learning_rate(), 0.01); gd.set_learning_rate(0.05); EXPECT_DOUBLE_EQ(gd.learning_rate(), 0.05); } // ─────────────────────────────────────────────────── // fit_surface_to_points // ─────────────────────────────────────────────────── TEST(SdfOptimize, FitSurfaceToPoints_Alias) { auto initial = SdfNode::sphere(0.5); std::vector points; points.emplace_back(1, 0, 0); points.emplace_back(0, 1, 0); points.emplace_back(0, 0, 1); auto result = fit_surface_to_points(initial, points, 0.01, 10); // At minimum, should produce a valid result EXPECT_GE(result.iterations, 1); } // ─────────────────────────────────────────────────── // find_accessible_point // ─────────────────────────────────────────────────── TEST(SdfOptimize, FindAccessiblePoint_Sphere) { auto shape = SdfNode::sphere(1.0); Point3D bmin(-2, -2, -2); Point3D bmax(2, 2, 2); Vector3D approach_dir(-1, 0, 0); // from -X direction Point3D result = find_accessible_point(shape, approach_dir, bmin, bmax, 16); // Should find some point (won't crash) EXPECT_TRUE(true); }