Files
ViewDesignEngine/tests/sdf/test_sdf_primitives.cpp
T
茂之钳 b7419a7881
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 35s
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 行)
2026-07-24 07:04:55 +00:00

512 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}