feat(v4.0): assembly interference checking — GJK + AABB broad-phase
This commit is contained in:
@@ -15,3 +15,4 @@ add_vde_test(test_brep_heal)
|
||||
add_vde_test(test_brep_drawing)
|
||||
add_vde_test(test_assembly_instance)
|
||||
add_vde_test(test_constraint_solver_3d)
|
||||
add_vde_test(test_interference_check)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/interference_check.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/core/transform.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// check_pair — basic scenarios
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(InterferenceCheckTest, Pair_SeparatedBoxes_NoInterference) {
|
||||
auto box_a = make_box(2, 2, 2);
|
||||
auto box_b = make_box(2, 2, 2);
|
||||
Transform3D ta = translate(0, 0, 5); // box_a at z=5
|
||||
Transform3D tb = translate(0, 0, -5); // box_b at z=-5
|
||||
|
||||
auto result = check_pair(box_a, box_b, ta, tb);
|
||||
EXPECT_FALSE(result.interfering);
|
||||
EXPECT_EQ(result.penetration, 0.0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Pair_OverlappingBoxes_HasInterference) {
|
||||
auto box_a = make_box(2, 2, 2); // [-1,1]³
|
||||
auto box_b = make_box(2, 2, 2);
|
||||
Transform3D ta = translate(0.5, 0, 0); // shifted +0.5 in X
|
||||
Transform3D tb = Transform3D::Identity();
|
||||
|
||||
auto result = check_pair(box_a, box_b, ta, tb);
|
||||
EXPECT_TRUE(result.interfering);
|
||||
EXPECT_GT(result.penetration, 0.0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Pair_TouchingBoxes_Interference) {
|
||||
auto box_a = make_box(2, 2, 2);
|
||||
auto box_b = make_box(2, 2, 2);
|
||||
// Box A from [-1,1], Box B from [1,3] — touching at x=1
|
||||
Transform3D ta = Transform3D::Identity();
|
||||
Transform3D tb = translate(2, 0, 0);
|
||||
|
||||
auto result = check_pair(box_a, box_b, ta, tb);
|
||||
// Touching faces — GJK may or may not detect this as interference
|
||||
// depending on numerical precision
|
||||
SUCCEED(); // just verify no crash
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Pair_ContainedSphere_Interference) {
|
||||
auto box = make_box(4, 4, 4);
|
||||
auto sphere = make_sphere(1.0);
|
||||
Transform3D t_box = Transform3D::Identity();
|
||||
Transform3D t_sphere = Transform3D::Identity(); // sphere at origin inside box
|
||||
|
||||
auto result = check_pair(box, sphere, t_box, t_sphere);
|
||||
EXPECT_TRUE(result.interfering);
|
||||
EXPECT_GT(result.overlap_volume, 0.0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Pair_DisjointCylinders_NoInterference) {
|
||||
auto cyl_a = make_cylinder(1.0, 4.0);
|
||||
auto cyl_b = make_cylinder(1.0, 4.0);
|
||||
Transform3D ta = translate(-5, 0, 0);
|
||||
Transform3D tb = translate(5, 0, 0);
|
||||
|
||||
auto result = check_pair(cyl_a, cyl_b, ta, tb);
|
||||
EXPECT_FALSE(result.interfering);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// check_interference — assembly-level
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_EmptyAssembly_NoPairs) {
|
||||
Assembly assy("empty");
|
||||
auto result = check_interference(assy);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
EXPECT_EQ(result.total_pairs_checked, 0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_SinglePart_NoPairs) {
|
||||
Assembly assy("single");
|
||||
assy.root.add_part("box", make_box(2, 2, 2));
|
||||
auto result = check_interference(assy);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
EXPECT_EQ(result.total_pairs_checked, 0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_SeparatedParts_NoInterference) {
|
||||
Assembly assy("separated");
|
||||
assy.root.add_part("left", make_box(2, 2, 2), translate(-5, 0, 0));
|
||||
assy.root.add_part("right", make_box(2, 2, 2), translate(5, 0, 0));
|
||||
|
||||
auto result = check_interference(assy);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
EXPECT_EQ(result.total_pairs_checked, 1);
|
||||
EXPECT_EQ(result.interfering_pairs, 0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_OverlappingParts_HasInterference) {
|
||||
Assembly assy("overlap");
|
||||
assy.root.add_part("bottom", make_box(4, 4, 2));
|
||||
assy.root.add_part("top", make_box(2, 2, 2), translate(0, 0, 1));
|
||||
|
||||
auto result = check_interference(assy);
|
||||
EXPECT_TRUE(result.has_interference);
|
||||
EXPECT_GT(result.conflicts.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_ThreeParts_CheckAllPairs) {
|
||||
Assembly assy("three");
|
||||
// Three boxes spread out — no interference
|
||||
assy.root.add_part("A", make_box(2, 2, 2), translate(-5, 0, 0));
|
||||
assy.root.add_part("B", make_box(2, 2, 2), translate(0, -5, 0));
|
||||
assy.root.add_part("C", make_box(2, 2, 2), translate(0, 5, 0));
|
||||
|
||||
auto result = check_interference(assy);
|
||||
// 3 parts → 3 pairs (A-B, A-C, B-C)
|
||||
EXPECT_EQ(result.total_pairs_checked, 3);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_CoarseOnly_FasterCheck) {
|
||||
Assembly assy("coarse");
|
||||
assy.root.add_part("box1", make_box(2, 2, 2), translate(0, 0, -2));
|
||||
assy.root.add_part("box2", make_box(2, 2, 2), translate(0, 0, 2));
|
||||
|
||||
auto result = check_interference(assy, true); // coarse only
|
||||
EXPECT_EQ(result.total_pairs_checked, 1);
|
||||
// AABB check: both boxes at z=±2 with size 2 → bounds [-1,1]×[-1,1]×[-3,-1] and [-1,1]×[-1,1]×[1,3]
|
||||
// These don't overlap → no interference
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_SubassemblyTraversal) {
|
||||
Assembly assy("root");
|
||||
auto* sub = assy.root.add_subassembly("group", translate(0, 5, 0));
|
||||
sub->add_part("sub_box", make_box(2, 2, 2));
|
||||
assy.root.add_part("main_box", make_box(4, 4, 4));
|
||||
|
||||
// sub is at y=5, sub_box inside it at identity → sub_box world = (0,5,0)
|
||||
// main_box is at origin
|
||||
// 2×2 box at y=5 vs 4×4 box at origin → separated by at least 1 unit
|
||||
auto result = check_interference(assy);
|
||||
EXPECT_EQ(result.total_pairs_checked, 1);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Assembly_InterferenceSummary) {
|
||||
Assembly assy("summary_test");
|
||||
assy.root.add_part("part_a", make_box(2, 2, 2));
|
||||
assy.root.add_part("part_b", make_box(2, 2, 2), translate(0.5, 0, 0));
|
||||
|
||||
auto result = check_interference(assy);
|
||||
auto summary = result.summary();
|
||||
EXPECT_FALSE(summary.empty());
|
||||
EXPECT_NE(summary.find("interference"), std::string::npos);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// penetration_depth
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(InterferenceCheckTest, Penetration_Separated_Zero) {
|
||||
auto box_a = make_box(2, 2, 2);
|
||||
auto box_b = make_box(2, 2, 2);
|
||||
double pen = penetration_depth(box_a, box_b,
|
||||
translate(-5, 0, 0), translate(5, 0, 0));
|
||||
EXPECT_EQ(pen, 0.0);
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, Penetration_Overlapping_Positive) {
|
||||
auto box_a = make_box(2, 2, 2);
|
||||
auto box_b = make_box(2, 2, 2);
|
||||
// box_a: origin, box_b: shifted 0.5 in X → overlapping
|
||||
double pen = penetration_depth(box_a, box_b,
|
||||
Transform3D::Identity(), translate(0.5, 0, 0));
|
||||
EXPECT_GE(pen, 0.0); // penetration should be ≥ 0
|
||||
}
|
||||
|
||||
TEST(InterferenceCheckTest, LargeAssembly_PerformanceAcceptable) {
|
||||
Assembly assy("large");
|
||||
// 10×10 grid of small boxes → 100 parts, 4950 pairs
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
for (int j = 0; j < 10; ++j) {
|
||||
assy.root.add_part(
|
||||
"box_" + std::to_string(i) + "_" + std::to_string(j),
|
||||
make_box(1, 1, 1),
|
||||
translate(3.0 * i, 3.0 * j, 0));
|
||||
}
|
||||
}
|
||||
|
||||
auto result = check_interference(assy);
|
||||
// All boxes spaced 3 units apart (size 1) → no interference
|
||||
EXPECT_EQ(result.total_pairs_checked, 4950);
|
||||
EXPECT_FALSE(result.has_interference);
|
||||
}
|
||||
Reference in New Issue
Block a user