286 lines
9.1 KiB
C++
286 lines
9.1 KiB
C++
#include "vde/sdf/sdf_tree.h"
|
|
#include <gtest/gtest.h>
|
|
#include <cmath>
|
|
|
|
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)
|
|
EXPECT_LT(evaluate(tree, Point3D(1.2, 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
|
|
}
|