#include "vde/sdf/sdf_tree.h" #include #include using namespace vde::sdf; using vde::core::Point3D; using vde::core::Vector3D; // ── Null/empty tree ────────────────────────────────────────────── TEST(SdfTree, NullNodeReturnsLargeValue) { SdfNodePtr null_node = nullptr; double d = evaluate(null_node, Point3D(0, 0, 0)); EXPECT_GT(d, 1e10); } // ── Primitive evaluation ───────────────────────────────────────── TEST(SdfTree, SphereInsideOutsideBoundary) { auto s = SdfNode::sphere(1.0); // Origin = inside double d_in = evaluate(s, Point3D(0, 0, 0)); EXPECT_LT(d_in, 0.0); EXPECT_NEAR(d_in, -1.0, 1e-6); // Surface (r=1 on X axis) double d_surf = evaluate(s, Point3D(1, 0, 0)); EXPECT_NEAR(d_surf, 0.0, 1e-6); // Outside double d_out = evaluate(s, Point3D(2, 0, 0)); EXPECT_GT(d_out, 0.0); EXPECT_NEAR(d_out, 1.0, 1e-6); } TEST(SdfTree, BoxEvaluation) { auto b = SdfNode::box(Point3D(1, 1, 1)); // Center = inside EXPECT_LT(evaluate(b, Point3D(0, 0, 0)), 0.0); // Surface EXPECT_NEAR(evaluate(b, Point3D(1, 0, 0)), 0.0, 1e-6); // Outside EXPECT_GT(evaluate(b, Point3D(2, 0, 0)), 0.0); } TEST(SdfTree, TorusEvaluation) { auto t = SdfNode::torus(1.0, 0.3); // Point on the ring centerline (should be -minor_radius = inside) double d = evaluate(t, Point3D(1, 0, 0)); EXPECT_LT(d, 0.0); EXPECT_NEAR(d, -0.3, 1e-6); } TEST(SdfTree, CylinderEvaluation) { auto c = SdfNode::cylinder(0.5, 1.0); EXPECT_LT(evaluate(c, Point3D(0, 0, 0)), 0.0); // center EXPECT_NEAR(evaluate(c, Point3D(0.5, 0, 0)), 0.0, 1e-6); // surface EXPECT_GT(evaluate(c, Point3D(1, 0, 0)), 0.0); // outside radius EXPECT_GT(evaluate(c, Point3D(0, 2, 0)), 0.0); // above cap } TEST(SdfTree, PlaneEvaluation) { auto p = SdfNode::plane(Vector3D(0, 1, 0), 0.0); // horizontal at y=0 EXPECT_GT(evaluate(p, Point3D(0, 1, 0)), 0.0); // above EXPECT_LT(evaluate(p, Point3D(0, -1, 0)), 0.0); // below EXPECT_NEAR(evaluate(p, Point3D(0, 0, 0)), 0.0, 1e-6); } // ── CSG boolean operations ─────────────────────────────────────── TEST(SdfTree, UnionSphereBox) { auto tree = SdfNode::op_union( SdfNode::sphere(1.0), SdfNode::box(Point3D(1, 1, 1)) ); // Point inside sphere EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // Point inside box (but outside sphere) — box surface at |x|=1 EXPECT_LT(evaluate(tree, Point3D(0.5, 0, 0)), 0.0); // Point outside both EXPECT_GT(evaluate(tree, Point3D(3, 0, 0)), 0.0); } TEST(SdfTree, IntersectionSphereBox) { auto tree = SdfNode::op_intersection( SdfNode::sphere(1.0), SdfNode::box(Point3D(0.5, 0.5, 0.5)) ); // Inside intersection region EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // Inside sphere but outside box EXPECT_GT(evaluate(tree, Point3D(0.8, 0, 0)), 0.0); } TEST(SdfTree, DifferenceSphereBox) { auto tree = SdfNode::op_difference( SdfNode::sphere(2.0), SdfNode::box(Point3D(1, 1, 1)) ); // Inside sphere, outside box → inside EXPECT_LT(evaluate(tree, Point3D(1.5, 0, 0)), 0.0); // Inside box (removed region) → outside EXPECT_GT(evaluate(tree, Point3D(0, 0, 0)), 0.0); } // ── Smooth boolean operations ──────────────────────────────────── TEST(SdfTree, SmoothUnion) { auto tree = SdfNode::smooth_union( SdfNode::sphere(1.0), SdfNode::sphere(1.0), 0.3 ); // Both spheres at origin → should be equivalent EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); } // ── Domain transforms ──────────────────────────────────────────── TEST(SdfTree, TranslatedSphere) { auto tree = SdfNode::translate( SdfNode::sphere(1.0), Point3D(2, 0, 0) ); // Original origin should be far away EXPECT_GT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // New center should be inside EXPECT_LT(evaluate(tree, Point3D(2, 0, 0)), 0.0); EXPECT_NEAR(evaluate(tree, Point3D(2, 0, 0)), -1.0, 1e-6); // Surface at new center + radius EXPECT_NEAR(evaluate(tree, Point3D(3, 0, 0)), 0.0, 1e-6); } TEST(SdfTree, ScaledSphere) { auto tree = SdfNode::scale( SdfNode::sphere(1.0), Point3D(2, 1, 1) ); // At (2,0,0) in world = (1,0,0) in local → on surface EXPECT_NEAR(evaluate(tree, Point3D(2, 0, 0)), 0.0, 1e-6); // At (0,0,0) in world = inside EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); } TEST(SdfTree, RepeatedSphere) { auto tree = SdfNode::repeat( SdfNode::sphere(0.3), Point3D(2, 2, 2) ); // Origin → mapped to (0,0,0) inside EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // (2,0,0) → mapped to (0,0,0) inside EXPECT_LT(evaluate(tree, Point3D(2, 0, 0)), 0.0); // Between copies → outside EXPECT_GT(evaluate(tree, Point3D(1, 1, 0)), 0.0); } TEST(SdfTree, MirroredX) { auto tree = SdfNode::mirror_x( SdfNode::sphere(1.0) ); // Both sides should have the sphere EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); EXPECT_LT(evaluate(tree, Point3D(-0.5, 0, 0)), 0.0); // Surface at ±1 EXPECT_NEAR(evaluate(tree, Point3D(1, 0, 0)), 0.0, 1e-6); EXPECT_NEAR(evaluate(tree, Point3D(-1, 0, 0)), 0.0, 1e-6); } // ── Nested operations ──────────────────────────────────────────── TEST(SdfTree, NestedUnionTwist) { auto inner = SdfNode::op_union( SdfNode::sphere(1.0), SdfNode::box(Point3D(0.6, 0.6, 0.6)) ); auto tree = SdfNode::twist(std::move(inner), 0.5); // Should still evaluate successfully double d = evaluate(tree, Point3D(0, 0.5, 0)); EXPECT_LT(d, 0.0); // interior of union } TEST(SdfTree, DeepNesting) { auto a = SdfNode::sphere(1.0); auto b = SdfNode::box(Point3D(0.5, 0.5, 0.5)); auto uni = SdfNode::op_union(std::move(a), std::move(b)); auto scaled = SdfNode::scale(std::move(uni), Point3D(2, 2, 2)); auto translated = SdfNode::translate(std::move(scaled), Point3D(3, 0, 0)); // Original origin → should be inside after inverse transform + union double d = evaluate(translated, Point3D(3, 0, 0)); EXPECT_LT(d, 0.0); } // ── Modifiers ──────────────────────────────────────────────────── TEST(SdfTree, RoundModifier) { auto tree = SdfNode::round( SdfNode::box(Point3D(1, 1, 1)), 0.2 ); // Center still inside EXPECT_LT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // Original corner point (1,1,0) should now be outside (rounded away) double d_corner = evaluate(tree, Point3D(1, 0, 0)); // Rounded box should have slightly larger radius at corners // The exact value depends on implementation, just verify it's farther double d_box = evaluate(SdfNode::box(Point3D(1, 1, 1)), Point3D(1, 0, 0)); EXPECT_NEAR(d_corner, d_box - 0.2, 1e-6); } TEST(SdfTree, OnionModifier) { auto tree = SdfNode::onion( SdfNode::sphere(2.0), 0.3 ); // Center should be outside (hollowed out) EXPECT_GT(evaluate(tree, Point3D(0, 0, 0)), 0.0); // Shell surface should exist EXPECT_NEAR(evaluate(tree, Point3D(1.7, 0, 0)), 0.0, 1e-6); } // ── Bounds estimation ──────────────────────────────────────────── TEST(SdfTree, EstimateBoundsSphere) { auto s = SdfNode::sphere(2.0); Point3D b = estimate_bounds(s, 0.0); EXPECT_NEAR(b.x(), 2.0, 1e-6); EXPECT_NEAR(b.y(), 2.0, 1e-6); EXPECT_NEAR(b.z(), 2.0, 1e-6); } TEST(SdfTree, EstimateBoundsWithMargin) { auto s = SdfNode::sphere(2.0); Point3D b = estimate_bounds(s, 1.0); EXPECT_NEAR(b.x(), 3.0, 1e-6); } TEST(SdfTree, EstimateBoundsUnion) { auto tree = SdfNode::op_union( SdfNode::sphere(2.0), SdfNode::box(Point3D(3, 3, 3)) ); Point3D b = estimate_bounds(tree, 0.0); // Box half-extent norm = sqrt(27) ≈ 5.196 EXPECT_GT(b.norm(), 4.0); } TEST(SdfTree, EstimateBoundsTranslated) { auto tree = SdfNode::translate( SdfNode::sphere(1.0), Point3D(5, 0, 0) ); Point3D b = estimate_bounds(tree, 0.0); EXPECT_GT(b.norm(), 5.0); } // ── Tree traversal ─────────────────────────────────────────────── TEST(SdfTree, VisitCountsNodes) { auto tree = SdfNode::op_union( SdfNode::sphere(1.0), SdfNode::box(Point3D(1, 1, 1)) ); int count = 0; tree->visit([&count](const SdfNode&) { ++count; }); EXPECT_EQ(count, 3); // union + sphere + box }