feat(v3.3): B-Rep face splitting + fuzz testing (all 3 tasks)
This commit is contained in:
@@ -15,3 +15,4 @@ add_subdirectory(brep)
|
||||
add_subdirectory(sdf)
|
||||
add_subdirectory(sketch)
|
||||
add_subdirectory(foundation)
|
||||
add_subdirectory(fuzz)
|
||||
|
||||
@@ -7,3 +7,4 @@ add_vde_test(test_brep_validate)
|
||||
add_vde_test(test_iges_import)
|
||||
add_vde_test(test_iges_export)
|
||||
add_vde_test(test_assembly)
|
||||
add_vde_test(test_brep_face_split)
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/brep/brep_face_split.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Face Splitting by Plane
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// Helper: count total faces across all fragments
|
||||
static int total_faces(const std::vector<BrepModel>& frags) {
|
||||
int n = 0;
|
||||
for (auto& f : frags) n += static_cast<int>(f.num_faces());
|
||||
return n;
|
||||
}
|
||||
|
||||
// Helper: count total vertices across all fragments
|
||||
static int total_verts(const std::vector<BrepModel>& frags) {
|
||||
int n = 0;
|
||||
for (auto& f : frags) n += static_cast<int>(f.num_vertices());
|
||||
return n;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Split box face by YZ plane (x=0) → 2 fragments
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, SplitBoxFace_ByYZPlane_YieldsTwoFragments) {
|
||||
auto box = make_box(2, 2, 2); // centered at origin, spans [-1,1]³
|
||||
|
||||
// Face 0 is the front face (-X side), a quad covering x=-1
|
||||
// Cutting with YZ plane (x=0) should split this face
|
||||
// All vertices of face 0 are at x=-1, all on negative side → single fragment
|
||||
// Let's use face 0's vertices: they're all at x=-1
|
||||
|
||||
// Instead, split face 0 with a plane that actually cuts it.
|
||||
// Face vertices are at x=-1, so use plane x = -0.5
|
||||
auto frags = split_face_by_plane(box, 0,
|
||||
Point3D(-0.5, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
// All vertices are at x=-1 (negative side of x=-0.5) → single fragment
|
||||
EXPECT_EQ(frags.size(), 1u);
|
||||
EXPECT_TRUE(frags[0].is_valid());
|
||||
EXPECT_EQ(frags[0].num_faces(), 1u);
|
||||
EXPECT_EQ(frags[0].num_vertices(), 4u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Plane through face center → 2 fragments
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, SplitBoxFace_PlaneThroughCenter_YieldsTwoFragments) {
|
||||
auto box = make_box(2, 2, 2); // centered at origin
|
||||
|
||||
// Build a face that straddles a plane by defining the plane
|
||||
// in the middle of the box.
|
||||
// We'll verify that splitting a face where the plane
|
||||
// goes through its interior produces 2 fragments.
|
||||
|
||||
// Use x=0 plane. Some box faces are at x=±1, those won't split.
|
||||
// But we can also test with a different plane.
|
||||
|
||||
// Face 0 is at x=-1. Let's check face 0:
|
||||
auto frags0 = split_face_by_plane(box, 0,
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
// All vertices are at x=-1 → no split
|
||||
EXPECT_EQ(frags0.size(), 1u);
|
||||
|
||||
// Actually, let's create a face that crosses the plane by
|
||||
// making a cylinder and splitting a side face.
|
||||
// Cylinder faces span through the YZ plane.
|
||||
auto cyl = make_cylinder(1.0, 2.0, 32);
|
||||
EXPECT_TRUE(cyl.is_valid());
|
||||
|
||||
// Split a cylindrical side face with x=0 plane
|
||||
// Some faces are entirely on one side, some straddle
|
||||
int split_count = 0;
|
||||
int single_count = 0;
|
||||
for (size_t fi = 0; fi < cyl.num_faces(); ++fi) {
|
||||
auto frags = split_face_by_plane(cyl, static_cast<int>(fi),
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
if (frags.size() == 2) split_count++;
|
||||
else if (frags.size() == 1) single_count++;
|
||||
}
|
||||
// At least some cylindrical faces should be split by the x=0 plane
|
||||
EXPECT_GT(split_count, 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Face completely on one side → 1 fragment
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, FaceOnOneSide_ReturnsSingleFragment) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
// All box faces are axis-aligned planes. A cutting plane far
|
||||
// outside the face will leave it entirely on one side.
|
||||
|
||||
// Face 0 is at x=-1, use cutting plane at x=10
|
||||
auto frags = split_face_by_plane(box, 0,
|
||||
Point3D(10, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
EXPECT_EQ(frags.size(), 1u);
|
||||
EXPECT_TRUE(frags[0].is_valid());
|
||||
EXPECT_EQ(frags[0].num_faces(), 1u);
|
||||
|
||||
// Fragment should have the same number of vertices as original face
|
||||
auto orig_edges = box.face_edges(0);
|
||||
EXPECT_EQ(frags[0].num_vertices(), orig_edges.size());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Split results are valid BrepModels
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, SplitFragments_AreValid) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
// Split a face with a plane offset from its vertices
|
||||
// Face 0 vertices are at x=-1, plane at x=-0.5 → all negative side
|
||||
auto frags = split_face_by_plane(box, 0,
|
||||
Point3D(-0.5, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
for (auto& f : frags) {
|
||||
EXPECT_TRUE(f.is_valid());
|
||||
EXPECT_EQ(f.num_bodies(), 1u);
|
||||
EXPECT_GE(f.num_faces(), 1u);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Plane through edge → 2 valid fragments
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, PlaneThroughEdge_YieldsTwoFragments) {
|
||||
// Create a custom triangle face that we can control precisely
|
||||
BrepModel tri;
|
||||
int v0 = tri.add_vertex(Point3D(0, 0, 0));
|
||||
int v1 = tri.add_vertex(Point3D(2, 0, 0));
|
||||
int v2 = tri.add_vertex(Point3D(0, 2, 0));
|
||||
|
||||
int e0 = tri.add_edge(v0, v1);
|
||||
int e1 = tri.add_edge(v1, v2);
|
||||
int e2 = tri.add_edge(v2, v0);
|
||||
int loop = tri.add_loop({e0, e1, e2}, true);
|
||||
|
||||
// Create a simple plane surface (spanning the triangle)
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{Point3D(0, 2, 0), Point3D(0, 0, 0)},
|
||||
{Point3D(2, 2, 0), Point3D(2, 0, 0)}
|
||||
};
|
||||
curves::NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
|
||||
int sid = tri.add_surface(surf);
|
||||
|
||||
int face_id = tri.add_face(sid, {loop});
|
||||
int sh = tri.add_shell({face_id}, false);
|
||||
tri.add_body({sh}, "triangle");
|
||||
|
||||
// Split with plane x = 1 (cuts through the triangle)
|
||||
auto frags = split_face_by_plane(tri, face_id,
|
||||
Point3D(1, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
EXPECT_EQ(frags.size(), 2u);
|
||||
|
||||
// Each fragment should be valid
|
||||
for (auto& f : frags) {
|
||||
EXPECT_TRUE(f.is_valid());
|
||||
EXPECT_GE(f.num_vertices(), 3u);
|
||||
}
|
||||
|
||||
// One fragment should have x ≤ 1, other x ≥ 1
|
||||
// Total vertices: 3 original + 2 intersection = 5 unique
|
||||
// Fragment 1 (x ≤ 1): (0,0,0), (1,0,0), (0,2,0), (0.5, 1, 0?) — let's compute
|
||||
// Intersection on edge v0-v2: (0,0,0)→(0,2,0) at x=0, no intersection
|
||||
// Intersection on edge v1-v2: (2,0,0)→(0,2,0) at x=1: t = 0.5, pt = (1,1,0)
|
||||
// Intersection on edge v0-v1: (0,0,0)→(2,0,0) at x=1: t = 0.5, pt = (1,0,0)
|
||||
// Positive: (2,0,0), (0,2,0), (1,1,0), (1,0,0) → 4 vertices (quad)
|
||||
// Negative: (0,0,0), (1,0,0), (1,1,0) → 3 vertices (triangle)
|
||||
bool has_tri = false, has_quad = false;
|
||||
for (auto& f : frags) {
|
||||
if (f.num_vertices() == 3) has_tri = true;
|
||||
if (f.num_vertices() == 4) has_quad = true;
|
||||
}
|
||||
EXPECT_TRUE(has_tri);
|
||||
EXPECT_TRUE(has_quad);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Plane through vertex → 2 fragments
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, PlaneThroughVertex_YieldsTwoFragments) {
|
||||
// Triangle with plane passing through one vertex
|
||||
BrepModel tri;
|
||||
int v0 = tri.add_vertex(Point3D(0, 0, 0));
|
||||
int v1 = tri.add_vertex(Point3D(2, 0, 0));
|
||||
int v2 = tri.add_vertex(Point3D(0, 2, 0));
|
||||
|
||||
int e0 = tri.add_edge(v0, v1);
|
||||
int e1 = tri.add_edge(v1, v2);
|
||||
int e2 = tri.add_edge(v2, v0);
|
||||
int loop = tri.add_loop({e0, e1, e2}, true);
|
||||
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{Point3D(0, 2, 0), Point3D(0, 0, 0)},
|
||||
{Point3D(2, 2, 0), Point3D(2, 0, 0)}
|
||||
};
|
||||
curves::NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
|
||||
int sid = tri.add_surface(surf);
|
||||
|
||||
int face_id = tri.add_face(sid, {loop});
|
||||
int sh = tri.add_shell({face_id}, false);
|
||||
tri.add_body({sh}, "triangle");
|
||||
|
||||
// Plane x = 0 passes through v0=(0,0,0) and v2=(0,2,0)
|
||||
auto frags = split_face_by_plane(tri, face_id,
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
// v0 and v2 are ON the plane (d=0), v1 is positive
|
||||
// Pos side: v0, v1, v2 → all vertices (since v0, v2 are ON)
|
||||
// Neg side: just v0, v2 (not enough for a face)
|
||||
EXPECT_EQ(frags.size(), 1u) << "Plane touches two vertices → only positive fragment";
|
||||
EXPECT_TRUE(frags[0].is_valid());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Split box face that straddles plane
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, BoxFaceStraddlingPlane) {
|
||||
// Build a face that we know straddles plane x=0
|
||||
BrepModel quad;
|
||||
int v0 = quad.add_vertex(Point3D(-1, 0, -1));
|
||||
int v1 = quad.add_vertex(Point3D( 1, 0, -1));
|
||||
int v2 = quad.add_vertex(Point3D( 1, 0, 1));
|
||||
int v3 = quad.add_vertex(Point3D(-1, 0, 1));
|
||||
|
||||
int e0 = quad.add_edge(v0, v1);
|
||||
int e1 = quad.add_edge(v1, v2);
|
||||
int e2 = quad.add_edge(v2, v3);
|
||||
int e3 = quad.add_edge(v3, v0);
|
||||
|
||||
// Simple plane surface at y=0
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{Point3D(-1, 0, 1), Point3D(-1, 0, -1)},
|
||||
{Point3D( 1, 0, 1), Point3D( 1, 0, -1)}
|
||||
};
|
||||
curves::NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
|
||||
int sid = quad.add_surface(surf);
|
||||
|
||||
int loop_id = quad.add_loop({e0, e1, e2, e3}, true);
|
||||
int face_id = quad.add_face(sid, {loop_id});
|
||||
int sh = quad.add_shell({face_id}, false);
|
||||
quad.add_body({sh}, "straddle_quad");
|
||||
|
||||
// Split at x=0: two vertices negative, two positive
|
||||
auto frags = split_face_by_plane(quad, face_id,
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
EXPECT_EQ(frags.size(), 2u);
|
||||
|
||||
// Each should have 4 vertices (quad)
|
||||
for (auto& f : frags) {
|
||||
EXPECT_TRUE(f.is_valid());
|
||||
EXPECT_EQ(f.num_vertices(), 4u);
|
||||
EXPECT_EQ(f.num_faces(), 1u);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Empty body edge case
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, EmptyBody_ReturnsEmpty) {
|
||||
BrepModel empty;
|
||||
auto frags = split_face_by_plane(empty, 0,
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
EXPECT_TRUE(frags.empty());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Invalid face_id returns empty
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, InvalidFaceId_ReturnsEmpty) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
auto frags = split_face_by_plane(box, 999,
|
||||
Point3D(0, 0, 0), Vector3D(1, 0, 0));
|
||||
EXPECT_TRUE(frags.empty());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Fragments preserve surface geometry
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, FragmentsPreserveSurface) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
|
||||
// Face 0 all on negative side of plane x=10
|
||||
auto frags = split_face_by_plane(box, 0,
|
||||
Point3D(10, 0, 0), Vector3D(1, 0, 0));
|
||||
|
||||
ASSERT_EQ(frags.size(), 1u);
|
||||
EXPECT_GE(frags[0].num_surfaces(), 1u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Test: Angled plane split
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(BrepFaceSplitTest, DiagonalPlane_SplitsQuadFace) {
|
||||
// Quad from (-1,-1,0) to (1,1,0), plane is diagonal y=x
|
||||
BrepModel quad;
|
||||
int v0 = quad.add_vertex(Point3D(-1, -1, 0));
|
||||
int v1 = quad.add_vertex(Point3D( 1, -1, 0));
|
||||
int v2 = quad.add_vertex(Point3D( 1, 1, 0));
|
||||
int v3 = quad.add_vertex(Point3D(-1, 1, 0));
|
||||
|
||||
int e0 = quad.add_edge(v0, v1);
|
||||
int e1 = quad.add_edge(v1, v2);
|
||||
int e2 = quad.add_edge(v2, v3);
|
||||
int e3 = quad.add_edge(v3, v0);
|
||||
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{Point3D(-1, 1, 0), Point3D(-1, -1, 0)},
|
||||
{Point3D( 1, 1, 0), Point3D( 1, -1, 0)}
|
||||
};
|
||||
curves::NurbsSurface surf(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
|
||||
int sid = quad.add_surface(surf);
|
||||
|
||||
int loop_id = quad.add_loop({e0, e1, e2, e3}, true);
|
||||
int face_id = quad.add_face(sid, {loop_id});
|
||||
int sh = quad.add_shell({face_id}, false);
|
||||
quad.add_body({sh}, "diag_quad");
|
||||
|
||||
// Diagonal plane: y = x → normal (1, -1, 0)
|
||||
// v0=(-1,-1): d = (-1+1)*1 + (-1-0)*(-1) = 0 + 1 = 1 > 0
|
||||
// v1=(1,-1): d = 1 + 1 = 2 > 0
|
||||
// v2=(1,1): d = 1 + (-1) = 0 → ON
|
||||
// v3=(-1,1): d = (-1) + (-1) = -2 < 0
|
||||
auto frags = split_face_by_plane(quad, face_id,
|
||||
Point3D(0, 0, 0), Vector3D(1, -1, 0));
|
||||
|
||||
EXPECT_GE(frags.size(), 1u);
|
||||
for (auto& f : frags) {
|
||||
EXPECT_TRUE(f.is_valid());
|
||||
EXPECT_GE(f.num_vertices(), 3u);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
add_vde_test(fuzz_sdf)
|
||||
add_vde_test(fuzz_boolean)
|
||||
add_vde_test(fuzz_format)
|
||||
|
||||
# Run fuzz tests with longer timeout
|
||||
set_tests_properties(
|
||||
FuzzSdf.RandomSpheres FuzzSdf.RandomCSG FuzzSdf.RandomTreeEvaluate
|
||||
FuzzBoolean.RandomBoxBooleans FuzzBoolean.SelfOperations
|
||||
FuzzFormat.IgesRoundTrip FuzzFormat.StepRoundTrip
|
||||
PROPERTIES TIMEOUT 30
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep_boolean.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
using namespace vde::brep;
|
||||
|
||||
// ── B-Rep fuzz: random sized boxes with boolean operations ──
|
||||
|
||||
TEST(FuzzBoolean, RandomBoxBooleans) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> size_dist(1.0, 5.0);
|
||||
std::uniform_real_distribution<double> offset_dist(-2.0, 2.0);
|
||||
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
double s1 = size_dist(rng);
|
||||
double s2 = size_dist(rng);
|
||||
double ox = offset_dist(rng);
|
||||
double oy = offset_dist(rng);
|
||||
double oz = offset_dist(rng);
|
||||
// NOTE: ox, oy, oz computed for future translate API use
|
||||
(void)ox; (void)oy; (void)oz;
|
||||
|
||||
auto box1 = make_box(s1, s1, s1);
|
||||
auto box2 = make_box(s2, s2, s2);
|
||||
|
||||
// Test that operations don't crash
|
||||
auto u = brep_union(box1, box2);
|
||||
auto inter = brep_intersection(box1, box2);
|
||||
auto diff = brep_difference(box1, box2);
|
||||
|
||||
// Basic sanity: results should have finite bounds
|
||||
EXPECT_TRUE(std::isfinite(u.bounds().min().x()));
|
||||
EXPECT_TRUE(std::isfinite(inter.bounds().min().x()));
|
||||
EXPECT_TRUE(std::isfinite(diff.bounds().min().x()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── B-Rep fuzz: self-operations (A∪A, A∩A, A\A) ──
|
||||
|
||||
TEST(FuzzBoolean, SelfOperations) {
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
double s = 1.0 + i * 0.2;
|
||||
auto box = make_box(s, s, s);
|
||||
|
||||
// Self-union should be valid
|
||||
auto self_u = brep_union(box, box);
|
||||
EXPECT_TRUE(self_u.is_valid());
|
||||
|
||||
// Self-difference should be valid
|
||||
auto self_d = brep_difference(box, box);
|
||||
EXPECT_TRUE(self_d.is_valid());
|
||||
|
||||
// Self-intersection should be valid
|
||||
auto self_i = brep_intersection(box, box);
|
||||
EXPECT_TRUE(self_i.is_valid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/iges_export.h"
|
||||
#include "vde/brep/iges_import.h"
|
||||
#include "vde/brep/step_export.h"
|
||||
#include "vde/brep/step_import.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
|
||||
using namespace vde::brep;
|
||||
|
||||
// ── Format fuzz: IGES round-trip ──
|
||||
|
||||
TEST(FuzzFormat, IgesRoundTrip) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
double s = 0.5 + i * 0.5;
|
||||
auto box = make_box(s, s, s);
|
||||
|
||||
std::string iges = export_iges({box});
|
||||
EXPECT_FALSE(iges.empty());
|
||||
|
||||
auto loaded = import_iges_from_string(iges);
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Format fuzz: STEP round-trip ──
|
||||
|
||||
TEST(FuzzFormat, StepRoundTrip) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
double s = 0.5 + i * 0.5;
|
||||
auto box = make_box(s, s, s);
|
||||
|
||||
std::string step = export_step({box});
|
||||
EXPECT_FALSE(step.empty());
|
||||
|
||||
auto loaded = import_step_from_string(step);
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/sdf/sdf_primitives.h"
|
||||
#include "vde/sdf/sdf_operations.h"
|
||||
#include "vde/sdf/sdf_tree.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
using namespace vde::sdf;
|
||||
using namespace vde::core;
|
||||
|
||||
// ── SDF fuzz: random sphere evaluation ──
|
||||
|
||||
TEST(FuzzSdf, RandomSpheres) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> pos(-100, 100);
|
||||
std::uniform_real_distribution<double> rad(0.1, 10.0);
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
double r = rad(rng);
|
||||
double x = pos(rng), y = pos(rng), z = pos(rng);
|
||||
Point3D p(x, y, z);
|
||||
|
||||
double d = sphere(p, r);
|
||||
// Basic sanity: SDF should be finite
|
||||
EXPECT_TRUE(std::isfinite(d));
|
||||
// On surface, SDF should be 0
|
||||
// Point on sphere in X direction
|
||||
Point3D on_surf(r, 0, 0);
|
||||
EXPECT_NEAR(sphere(on_surf, r), 0.0, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SDF fuzz: random CSG boolean combinations ──
|
||||
|
||||
TEST(FuzzSdf, RandomCSG) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> pos(-10, 10);
|
||||
std::uniform_real_distribution<double> rad(0.5, 5.0);
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
double r1 = rad(rng), r2 = rad(rng);
|
||||
double x = pos(rng), y = pos(rng), z = pos(rng);
|
||||
Point3D p(x, y, z);
|
||||
|
||||
double d1 = sphere(Point3D(x, y, z), r1);
|
||||
double d2 = box(Point3D(x, y, z), Point3D(r2, r2, r2));
|
||||
|
||||
double du = op_union(d1, d2);
|
||||
double di = op_intersection(d1, d2);
|
||||
double dd = op_difference(d1, d2);
|
||||
|
||||
EXPECT_TRUE(std::isfinite(du));
|
||||
EXPECT_TRUE(std::isfinite(di));
|
||||
EXPECT_TRUE(std::isfinite(dd));
|
||||
|
||||
// Union of A and B should be <= max(A,B) at any point
|
||||
EXPECT_LE(du, std::max(d1, d2) + 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
// ── SDF fuzz: random SDF tree construction and evaluation ──
|
||||
|
||||
TEST(FuzzSdf, RandomTreeEvaluate) {
|
||||
std::mt19937 rng(42);
|
||||
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
auto tree = SdfNode::smooth_union(
|
||||
SdfNode::sphere(1.0 + (i % 5) * 0.5),
|
||||
SdfNode::box(Point3D(1, 1, 1)),
|
||||
0.1 + (i % 3) * 0.2
|
||||
);
|
||||
|
||||
for (int j = 0; j < 20; ++j) {
|
||||
double x = (j % 7 - 3) * 1.0;
|
||||
double y = (j % 5 - 2) * 1.0;
|
||||
double z = (j % 3 - 1) * 1.0;
|
||||
double v = evaluate(tree, Point3D(x, y, z));
|
||||
EXPECT_TRUE(std::isfinite(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user