feat(sdf): S11 — SDF 隐式建模完整模块
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 35s

S11-A: SDF 图元库
- 14 种基础形状(inline header)
- sphere/box/round_box/torus/capsule/cylinder/cone/plane/ellipsoid
- triangular_prism/hex_prism/link/wedge
- 2D 挤出(extrusion/extrusion_bounded/revolution)

S11-B: SDF 操作 + 域变形
- 锐利布尔: union/intersection/difference
- 平滑布尔: smooth_union/smooth_intersection/smooth_difference
- 修饰器: round/onion
- 域变形: repeat/mirror/rotate/translate/scale/twist/bend/elongate/displace/cheap_bend
- 24 项测试

S11-C: CSG 表达式树 + 网格转换 + Python 绑定
- SdfNode 树结构 — 工厂构造函数,递归求值
- sdf_to_mesh — 基于 marching_cubes 的 SDF→网格
- bind_sdf — pybind11 Python 绑定

文件: 14 文件,2,545 行(测试 1,296 行)
This commit is contained in:
茂之钳
2026-07-24 07:04:55 +00:00
parent 6d3f8fc8b3
commit b7419a7881
17 changed files with 1997 additions and 115 deletions
+4 -9
View File
@@ -1,9 +1,4 @@
function(add_vde_sdf_test name)
add_executable(${name} ${name}.cpp)
target_link_libraries(${name} PRIVATE vde_sdf GTest::gtest GTest::gtest_main)
target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/include)
gtest_discover_tests(${name})
endfunction()
add_vde_sdf_test(test_sdf_tree)
add_vde_sdf_test(test_sdf_to_mesh)
add_vde_test(test_sdf_primitives)
add_vde_test(test_sdf_operations)
add_vde_test(test_sdf_tree)
add_vde_test(test_sdf_to_mesh)
+511
View File
@@ -0,0 +1,511 @@
#include <gtest/gtest.h>
#include "vde/sdf/sdf_primitives.h"
#include <cmath>
using namespace vde::sdf;
using vde::core::Point3D;
using vde::core::Vector3D;
constexpr double EPS = 1e-9;
constexpr double SQRT2 = 1.4142135623730951;
constexpr double SQRT3 = 1.7320508075688772;
// ═══════════════════════════════════════════════════
// sphere
// ═══════════════════════════════════════════════════
TEST(SdfSphere, Outside) {
EXPECT_NEAR(sphere(Point3D(5, 0, 0), 3.0), 2.0, EPS);
EXPECT_NEAR(sphere(Point3D(0, 0, 0), 3.0), -3.0, EPS);
}
TEST(SdfSphere, OnSurface) {
EXPECT_NEAR(sphere(Point3D(3, 0, 0), 3.0), 0.0, EPS);
EXPECT_NEAR(sphere(Point3D(0, -3, 0), 3.0), 0.0, EPS);
}
TEST(SdfSphere, Inside) {
// Center of a radius-5 sphere: distance should be -5
EXPECT_NEAR(sphere(Point3D(0, 0, 0), 5.0), -5.0, EPS);
// Partway in
EXPECT_NEAR(sphere(Point3D(2, 0, 0), 5.0), -3.0, EPS);
}
TEST(SdfSphere, Degenerate) {
// Zero radius
EXPECT_NEAR(sphere(Point3D(1, 0, 0), 0.0), 1.0, EPS);
EXPECT_NEAR(sphere(Point3D(0, 0, 0), 0.0), 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// box
// ═══════════════════════════════════════════════════
TEST(SdfBox, Outside) {
Point3D b(1, 1, 1);
// Outside corner
double d = box(Point3D(2, 2, 2), b);
EXPECT_GT(d, 0.0);
EXPECT_NEAR(d, std::sqrt(3.0), EPS);
}
TEST(SdfBox, OnSurface) {
Point3D b(1, 1, 1);
EXPECT_NEAR(box(Point3D(1, 0, 0), b), 0.0, EPS);
EXPECT_NEAR(box(Point3D(0, 1, 0), b), 0.0, EPS);
EXPECT_NEAR(box(Point3D(0, 0, 1), b), 0.0, EPS);
EXPECT_NEAR(box(Point3D(-1, 0, 0), b), 0.0, EPS);
}
TEST(SdfBox, Inside) {
Point3D b(3, 3, 3);
// Center
EXPECT_NEAR(box(Point3D(0, 0, 0), b), -3.0, EPS);
// Mid-edge
double d = box(Point3D(2, 0, 0), b);
EXPECT_NEAR(d, -1.0, EPS);
}
TEST(SdfBox, Degenerate) {
Point3D zero(0, 0, 0);
double d = box(Point3D(1, 1, 1), zero);
EXPECT_NEAR(d, std::sqrt(3.0), EPS);
}
// ═══════════════════════════════════════════════════
// round_box
// ═══════════════════════════════════════════════════
TEST(SdfRoundBox, MatchesBoxAtZeroRadius) {
Point3D b(2, 3, 4);
for (double x = -5; x <= 5; x += 2.5) {
for (double y = -5; y <= 5; y += 2.5) {
for (double z = -5; z <= 5; z += 2.5) {
Point3D pt(x, y, z);
EXPECT_NEAR(round_box(pt, b, 0.0), box(pt, b), EPS);
}
}
}
}
TEST(SdfRoundBox, RoundedShiftsBoundary) {
Point3D b(1, 1, 1);
// On surface of round box with r=0.5: outside moves in by 0.5
double d_box = box(Point3D(1.5, 0, 0), b);
double d_rbox = round_box(Point3D(1.5, 0, 0), b, 0.5);
EXPECT_NEAR(d_rbox, d_box - 0.5, EPS);
}
// ═══════════════════════════════════════════════════
// torus
// ═══════════════════════════════════════════════════
TEST(SdfTorus, OnSurfaceMajor) {
// Point on major circle
double d = torus(Point3D(5, 0, 0), 5.0, 1.0);
EXPECT_NEAR(d, -1.0, EPS); // inside tube, at center of tube cross-section
}
TEST(SdfTorus, RingCenter_Inside) {
// Center of torus (inside the hole)
double d = torus(Point3D(0, 0, 0), 5.0, 1.0);
EXPECT_NEAR(d, 4.0, EPS); // distance from origin to tube center = 5, minus tube radius = 4
}
TEST(SdfTorus, OutsideTube) {
// Outside the whole shape
double d = torus(Point3D(0, 3, 0), 5.0, 1.0);
// Point (0,3,0): distance from Y axis = 0, so tube center at (5,0,0)
// Distance to tube center: sqrt(25 + 9) = sqrt(34) ≈ 5.83, minus 1 = 4.83
EXPECT_NEAR(d, std::sqrt(34.0) - 1.0, EPS);
}
TEST(SdfTorus, Degenerate) {
double d = torus(Point3D(3, 0, 0), 3.0, 0.0);
EXPECT_NEAR(d, 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// capsule
// ═══════════════════════════════════════════════════
TEST(SdfCapsule, OnAxis) {
Point3D a(0, 0, 0), b(0, 4, 0);
double r = 1.0;
EXPECT_NEAR(capsule(Point3D(0, 0, 0), a, b, r), -r, EPS);
EXPECT_NEAR(capsule(Point3D(0, 0, 1.0), a, b, r), 0.0, EPS);
EXPECT_NEAR(capsule(Point3D(0, 0, 0), a, b, 0.0), 0.0, EPS);
}
TEST(SdfCapsule, OutsideCylinder) {
Point3D a(0, 0, 0), b(0, 4, 0);
double r = 1.0;
double d = capsule(Point3D(3, 2, 0), a, b, r);
EXPECT_NEAR(d, 2.0, EPS);
}
TEST(SdfCapsule, Midpoint) {
Point3D a(0, 0, 0), b(2, 0, 0);
double r = 0.5;
EXPECT_NEAR(capsule(Point3D(1, 0, 0), a, b, r), -0.5, EPS);
EXPECT_NEAR(capsule(Point3D(1, 0.5, 0), a, b, r), 0.0, EPS);
}
TEST(SdfCapsule, Degenerate) {
Point3D pt(0, 0, 0);
// Zero-length capsule = sphere
double d = capsule(Point3D(2, 0, 0), pt, pt, 3.0);
EXPECT_NEAR(d, -1.0, EPS);
}
// ═══════════════════════════════════════════════════
// cylinder
// ═══════════════════════════════════════════════════
TEST(SdfCylinder, OnSurface) {
double d = cylinder(Point3D(2, 0, 0), 2.0, 4.0);
EXPECT_NEAR(d, 0.0, EPS);
}
TEST(SdfCylinder, Inside) {
double d = cylinder(Point3D(0, 0, 0), 2.0, 4.0);
EXPECT_NEAR(d, -2.0, EPS);
}
TEST(SdfCylinder, OutsideCap) {
// Above top cap
double d = cylinder(Point3D(0, 3, 0), 1.0, 4.0);
EXPECT_NEAR(d, 1.0, EPS);
}
TEST(SdfCylinder, OutsideSide) {
// Outside the side
double d = cylinder(Point3D(3, 0, 0), 1.0, 4.0);
EXPECT_NEAR(d, 2.0, EPS);
}
TEST(SdfCylinder, Degenerate) {
// Zero radius = line segment
double d = cylinder(Point3D(0, 0, 0), 0.0, 4.0);
EXPECT_NEAR(d, 0.0, EPS);
// Zero height
double d2 = cylinder(Point3D(0, 0, 0), 2.0, 0.0);
EXPECT_NEAR(d2, 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// cone
// ═══════════════════════════════════════════════════
TEST(SdfCone, Apex) {
// Apex at y = height/2 = 2
double d = cone(Point3D(0, 2, 0), std::atan(0.5), 4.0);
EXPECT_NEAR(d, 0.0, EPS);
}
TEST(SdfCone, OnBaseEdge) {
// Base at y = -height/2 = -2, base_radius = 4 * tan(atan(0.5)) = 2
double d = cone(Point3D(2, -2, 0), std::atan(0.5), 4.0);
EXPECT_NEAR(d, 0.0, 1e-6);
}
TEST(SdfCone, Inside) {
// Inside the cone body
double d = cone(Point3D(0, 0, 0), std::atan(0.5), 4.0);
EXPECT_LT(d, 0.0);
}
TEST(SdfCone, Outside) {
// Outside near base
double d = cone(Point3D(3, -2, 0), std::atan(0.5), 4.0);
EXPECT_GT(d, 0.0);
}
TEST(SdfCone, BelowBase) {
// Below the base plane
double d = cone(Point3D(0, -3, 0), std::atan(0.5), 4.0);
EXPECT_GT(d, 0.0);
}
// ═══════════════════════════════════════════════════
// plane
// ═══════════════════════════════════════════════════
TEST(SdfPlane, Above) {
Vector3D n(0, 1, 0);
EXPECT_NEAR(plane(Point3D(0, 5, 0), n, 0.0), 5.0, EPS);
}
TEST(SdfPlane, Below) {
Vector3D n(0, 1, 0);
EXPECT_NEAR(plane(Point3D(0, -3, 0), n, 0.0), -3.0, EPS);
}
TEST(SdfPlane, WithOffset) {
Vector3D n(0, 1, 0);
EXPECT_NEAR(plane(Point3D(0, 5, 0), n, 2.0), 3.0, EPS);
EXPECT_NEAR(plane(Point3D(0, 1, 0), n, 2.0), -1.0, EPS);
}
// ═══════════════════════════════════════════════════
// ellipsoid
// ═══════════════════════════════════════════════════
TEST(SdfEllipsoid, SphereCase) {
// Ellipsoid with equal radii = sphere
Point3D radii(3, 3, 3);
double d = ellipsoid(Point3D(3, 0, 0), radii);
EXPECT_NEAR(d, 0.0, 1e-6);
}
TEST(SdfEllipsoid, Inside) {
Point3D radii(3, 2, 1);
double d = ellipsoid(Point3D(0, 0, 0), radii);
EXPECT_NEAR(d, -1.0, 1e-6);
}
TEST(SdfEllipsoid, OnSurface) {
// At (3,0,0): scaled = (1,0,0), |scaled|=1 → on surface
Point3D radii(3, 2, 1);
double d = ellipsoid(Point3D(3, 0, 0), radii);
EXPECT_NEAR(d, 0.0, 1e-6);
d = ellipsoid(Point3D(0, 2, 0), radii);
EXPECT_NEAR(d, 0.0, 1e-6);
d = ellipsoid(Point3D(0, 0, 1), radii);
EXPECT_NEAR(d, 0.0, 1e-6);
}
// ═══════════════════════════════════════════════════
// triangular_prism
// ═══════════════════════════════════════════════════
TEST(SdfTriangularPrism, OnVertex) {
Point3D a(0, 0, 0), b(4, 0, 0), c(2, 3, 0);
double d = triangular_prism(Point3D(0, 0, 0), a, b, c, 2.0);
EXPECT_NEAR(d, -1.0, 1e-6);
}
TEST(SdfTriangularPrism, Inside) {
Point3D a(0, 0, 0), b(4, 0, 0), c(2, 3, 0);
// Centroid of triangle, mid-Z
double d = triangular_prism(Point3D(2, 1, 0), a, b, c, 4.0);
EXPECT_LT(d, 0.0);
}
TEST(SdfTriangularPrism, OutsideXY) {
Point3D a(0, 0, 0), b(4, 0, 0), c(2, 3, 0);
// Point far outside the triangle but within Z range
double d = triangular_prism(Point3D(10, 10, 0), a, b, c, 2.0);
EXPECT_GT(d, 0.0);
}
TEST(SdfTriangularPrism, OutsideZ) {
Point3D a(0, 0, 0), b(4, 0, 0), c(2, 3, 0);
// Inside triangle but far above top cap
double d = triangular_prism(Point3D(2, 1, 5), a, b, c, 2.0);
EXPECT_NEAR(d, 4.0, 1e-6);
}
// ═══════════════════════════════════════════════════
// hex_prism
// ═══════════════════════════════════════════════════
TEST(SdfHexPrism, Center) {
double d = hex_prism(Point3D(0, 0, 0), 2.0, 4.0);
EXPECT_LT(d, 0.0);
}
TEST(SdfHexPrism, OnVertex) {
// Vertex at distance r along X
double d = hex_prism(Point3D(2, 0, 0), 2.0, 2.0);
EXPECT_NEAR(d, 0.0, 2e-5);
}
TEST(SdfHexPrism, OutsideCap) {
double d = hex_prism(Point3D(0, 3, 0), 1.0, 4.0);
EXPECT_NEAR(d, 1.0, 1e-6);
}
TEST(SdfHexPrism, Degenerate) {
double d = hex_prism(Point3D(0, 0, 0), 0.0, 0.0);
EXPECT_NEAR(d, 0.0, 1e-6);
}
// ═══════════════════════════════════════════════════
// link
// ═══════════════════════════════════════════════════
TEST(SdfLink, AtCenterOfFirstTorus) {
// Length 4, so torus centers at x=±2
// Center of first torus tube: x=2+3=5? No, torus major_r=3
// Tube center of first torus at (2+3, 0, 0) = (5, 0, 0) in XZ plane
double d = link(Point3D(5, 0, 0), 4.0, 3.0, 1.0);
EXPECT_NEAR(d, -1.0, EPS);
}
TEST(SdfLink, BetweenToruses) {
// Point between the two toruses, inside the overlap region
double d = link(Point3D(0, 0, 0), 2.0, 2.0, 0.5);
// This is inside both toruses, so negative
EXPECT_LT(d, 0.0);
}
TEST(SdfLink, FarOutside) {
double d = link(Point3D(100, 0, 0), 4.0, 3.0, 1.0);
EXPECT_GT(d, 90.0);
}
// ═══════════════════════════════════════════════════
// infinite_cylinder
// ═══════════════════════════════════════════════════
TEST(SdfInfiniteCylinder, OnSurface) {
Vector3D axis(0, 1, 0);
double d = infinite_cylinder(Point3D(2, 5, 0), axis, 2.0);
EXPECT_NEAR(d, 0.0, EPS);
// Any Y should work (infinite)
d = infinite_cylinder(Point3D(2, 100, 0), axis, 2.0);
EXPECT_NEAR(d, 0.0, EPS);
}
TEST(SdfInfiniteCylinder, Inside) {
Vector3D axis(0, 1, 0);
double d = infinite_cylinder(Point3D(0, 0, 0), axis, 3.0);
EXPECT_NEAR(d, -3.0, EPS);
}
TEST(SdfInfiniteCylinder, Outside) {
Vector3D axis(0, 1, 0);
double d = infinite_cylinder(Point3D(5, 0, 0), axis, 1.0);
EXPECT_NEAR(d, 4.0, EPS);
}
TEST(SdfInfiniteCylinder, SkewAxis) {
Vector3D axis(1, 0, 0);
// Along X axis: distance = sqrt(y²+z²) - r
double d = infinite_cylinder(Point3D(0, 3, 4), axis, 5.0);
EXPECT_NEAR(d, 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// infinite_cone
// ═══════════════════════════════════════════════════
TEST(SdfInfiniteCone, AtApex) {
Point3D apex(0, 0, 0);
Vector3D axis(0, 1, 0);
double d = infinite_cone(apex, apex, axis, 0.5);
EXPECT_NEAR(d, 0.0, EPS);
}
TEST(SdfInfiniteCone, OnSurface) {
Point3D apex(0, 0, 0);
Vector3D axis(0, 1, 0);
double angle = std::atan(1.0); // 45° cone
// At y=2, radius should be 2 (tan45° = 1)
double d = infinite_cone(Point3D(2, 2, 0), apex, axis, angle);
EXPECT_NEAR(d, 0.0, 1e-6);
}
TEST(SdfInfiniteCone, Inside) {
Point3D apex(0, 0, 0);
Vector3D axis(0, 1, 0);
double angle = std::atan(2.0); // wide cone
// Point close to axis, should be inside
double d = infinite_cone(Point3D(1, 2, 0), apex, axis, angle);
EXPECT_LT(d, 0.0);
}
TEST(SdfInfiniteCone, Outside) {
Point3D apex(0, 0, 0);
Vector3D axis(0, 1, 0);
double angle = std::atan(0.5); // narrow cone
// Far from axis
double d = infinite_cone(Point3D(10, 2, 0), apex, axis, angle);
EXPECT_GT(d, 0.0);
}
TEST(SdfInfiniteCone, BehindApex) {
Point3D apex(0, 0, 0);
Vector3D axis(0, 1, 0);
double angle = std::atan(1.0);
// Behind apex (negative Y) → outside
double d = infinite_cone(Point3D(0, -1, 0), apex, axis, angle);
EXPECT_GT(d, 0.0);
}
// ═══════════════════════════════════════════════════
// wedge
// ═══════════════════════════════════════════════════
TEST(SdfWedge, CenterOfBoxHalf) {
// Wedge: box [-w/2,w/2]×[-h/2,h/2]×[-d/2,d/2] ∩ {z ≥ x}
// Point inside the wedge region
double d = wedge(Point3D(-0.5, 0.0, 1.0), 4.0, 4.0, 4.0);
EXPECT_LT(d, 0.0);
}
TEST(SdfWedge, OutsideDiagonal) {
// Point in box but on wrong side of diagonal (x > z)
double d = wedge(Point3D(1.5, 0.0, 0.0), 4.0, 4.0, 4.0);
EXPECT_GT(d, 0.0);
}
TEST(SdfWedge, OnDiagonalPlane) {
// On the diagonal plane z = x
double d = wedge(Point3D(1, 0, 1), 4.0, 4.0, 4.0);
EXPECT_NEAR(d, 0.0, 1e-6);
}
TEST(SdfWedge, OutsideBox) {
double d = wedge(Point3D(0, 10, 10), 4.0, 4.0, 4.0);
EXPECT_GT(d, 0.0);
}
// ═══════════════════════════════════════════════════
// extrusion
// ═══════════════════════════════════════════════════
TEST(SdfExtrusion, PassThrough) {
double sd2 = 3.5;
EXPECT_NEAR(extrusion(Point3D(1, 2, 100), sd2), 3.5, EPS);
EXPECT_NEAR(extrusion(Point3D(-5, 7, -3), -2.0), -2.0, EPS);
}
TEST(SdfExtrusion, Zero) {
EXPECT_NEAR(extrusion(Point3D(0, 0, 0), 0.0), 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// extrusion_bounded
// ═══════════════════════════════════════════════════
TEST(SdfExtrusionBounded, InsideSlabAndShape) {
// 2D SDF says outside (positive), Z within bounds → d = max(pos, inside_Z)
double d = extrusion_bounded(Point3D(0, 0, 0), -1.0, 2.0);
EXPECT_NEAR(d, -1.0, EPS);
}
TEST(SdfExtrusionBounded, OutsideSlab) {
// 2D SDF inside, but Z outside slab
double d = extrusion_bounded(Point3D(0, 0, 5), -1.0, 2.0);
EXPECT_NEAR(d, 3.0, EPS);
}
TEST(SdfExtrusionBounded, Zero) {
double d = extrusion_bounded(Point3D(0, 0, 0), 0.0, 0.0);
EXPECT_NEAR(d, 0.0, EPS);
}
// ═══════════════════════════════════════════════════
// revolution
// ═══════════════════════════════════════════════════
TEST(SdfRevolution, PassThrough) {
double sd2 = -1.5;
EXPECT_NEAR(revolution(Point3D(3, 2, 4), sd2, 1.0), -1.5, EPS);
}
TEST(SdfRevolution, Zero) {
EXPECT_NEAR(revolution(Point3D(0, 0, 0), 0.0, 0.0), 0.0, EPS);
}
+86
View File
@@ -0,0 +1,86 @@
#include "vde/sdf/sdf_to_mesh.h"
#include <gtest/gtest.h>
using namespace vde::sdf;
using core::Point3D;
TEST(SdfToMesh, SphereProducesValidMesh) {
auto sphere = SdfNode::sphere(2.0);
auto mesh = sdf_to_mesh(sphere, 32, 0.0);
EXPECT_GT(mesh.vertices.size(), 0u);
EXPECT_GT(mesh.triangles.size(), 0u);
// All vertices should be near the sphere surface (r ≈ 2)
for (const auto& v : mesh.vertices) {
double r = v.norm();
EXPECT_NEAR(r, 2.0, 1.0); // coarse resolution → loose tolerance
}
// Each triangle should have 3 indices
for (const auto& tri : mesh.triangles) {
for (int i = 0; i < 3; ++i) {
EXPECT_GE(tri[i], 0);
EXPECT_LT(tri[i], static_cast<int>(mesh.vertices.size()));
}
}
}
TEST(SdfToMesh, UnionOfTwoSpheres) {
auto s1 = SdfNode::translate(SdfNode::sphere(1.0), Point3D(-1.5, 0, 0));
auto s2 = SdfNode::translate(SdfNode::sphere(1.0), Point3D(1.5, 0, 0));
auto tree = SdfNode::op_union(std::move(s1), std::move(s2));
auto mesh = sdf_to_mesh(tree, 32, 0.0);
EXPECT_GT(mesh.vertices.size(), 0u);
EXPECT_GT(mesh.triangles.size(), 0u);
}
TEST(SdfToMesh, BoxProducesValidMesh) {
auto box = SdfNode::box(Point3D(1, 1, 1));
auto mesh = sdf_to_mesh(box, 32, 0.0);
EXPECT_GT(mesh.vertices.size(), 0u);
EXPECT_GT(mesh.triangles.size(), 0u);
}
TEST(SdfToMesh, HigherResolutionProducesMoreTriangles) {
auto sphere = SdfNode::sphere(1.0);
auto low = sdf_to_mesh(sphere, 16, 0.0);
auto high = sdf_to_mesh(sphere, 32, 0.0);
EXPECT_GT(high.vertices.size(), low.vertices.size());
}
TEST(SdfToMesh, LambdaSphere) {
auto mesh = sdf_to_mesh_lambda(
[](double x, double y, double z) {
return std::sqrt(x*x + y*y + z*z) - 2.0;
},
Point3D(-3, -3, -3), Point3D(3, 3, 3),
32, 0.0
);
EXPECT_GT(mesh.vertices.size(), 0u);
EXPECT_GT(mesh.triangles.size(), 0u);
}
TEST(SdfToMesh, LambdaBox) {
auto mesh = sdf_to_mesh_lambda(
[](double x, double y, double z) {
double dx = std::abs(x) - 1.0;
double dy = std::abs(y) - 1.0;
double dz = std::abs(z) - 1.0;
return std::sqrt(std::max(dx, 0.0) * std::max(dx, 0.0) +
std::max(dy, 0.0) * std::max(dy, 0.0) +
std::max(dz, 0.0) * std::max(dz, 0.0)) +
std::min(std::max({dx, dy, dz}), 0.0);
},
Point3D(-2, -2, -2), Point3D(2, 2, 2),
32, 0.0
);
EXPECT_GT(mesh.vertices.size(), 0u);
EXPECT_GT(mesh.triangles.size(), 0u);
}
+285
View File
@@ -0,0 +1,285 @@
#include "vde/sdf/sdf_tree.h"
#include <gtest/gtest.h>
#include <cmath>
using namespace vde::sdf;
using core::Point3D;
using 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.85, 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
}