feat(sdf): S11 — SDF 隐式建模完整模块
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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user