#include #include "vde/brep/euler_op.h" #include "vde/brep/modeling.h" #include "vde/brep/tolerance.h" using namespace vde::brep; using namespace vde::core; // ═══════════════════════════════════════════════════════════ // Helpers // ═══════════════════════════════════════════════════════════ /// Create a simple planar surface for tests static vde::curves::NurbsSurface make_test_plane(double w = 10.0, double h_val = 10.0) { std::vector> grid = { {{-w/2, -h_val/2, 0}, {w/2, -h_val/2, 0}}, {{-w/2, h_val/2, 0}, {w/2, h_val/2, 0}} }; return vde::curves::NurbsSurface(grid, {0,0,1,1}, {0,0,1,1}, {}, 1, 1); } /// Build a simple cube as BrepModel static BrepModel make_cube(double size = 10.0) { BrepModel body; double h = size / 2; int v000 = body.add_vertex({-h, -h, -h}); int v001 = body.add_vertex({-h, -h, h}); int v010 = body.add_vertex({-h, h, -h}); int v011 = body.add_vertex({-h, h, h}); int v100 = body.add_vertex({ h, -h, -h}); int v101 = body.add_vertex({ h, -h, h}); int v110 = body.add_vertex({ h, h, -h}); int v111 = body.add_vertex({ h, h, h}); // Bottom int eb0 = body.add_edge(v000, v100); int eb1 = body.add_edge(v100, v101); int eb2 = body.add_edge(v101, v001); int eb3 = body.add_edge(v001, v000); int lb = body.add_loop({eb0, eb1, eb2, eb3}, true); // Top int et0 = body.add_edge(v010, v011); int et1 = body.add_edge(v011, v111); int et2 = body.add_edge(v111, v110); int et3 = body.add_edge(v110, v010); int lt = body.add_loop({et0, et1, et2, et3}, true); // Front int ef0 = body.add_edge(v001, v101); int ef1 = body.add_edge(v101, v111); int ef2 = body.add_edge(v111, v011); int ef3 = body.add_edge(v011, v001); int lf = body.add_loop({ef0, ef1, ef2, ef3}, true); // Back int ek0 = body.add_edge(v100, v000); int ek1 = body.add_edge(v000, v010); int ek2 = body.add_edge(v010, v110); int ek3 = body.add_edge(v110, v100); int lk = body.add_loop({ek0, ek1, ek2, ek3}, true); // Left int el0 = body.add_edge(v000, v001); int el1 = body.add_edge(v001, v011); int el2 = body.add_edge(v011, v010); int el3 = body.add_edge(v010, v000); int ll = body.add_loop({el0, el1, el2, el3}, true); // Right int er0 = body.add_edge(v100, v110); int er1 = body.add_edge(v110, v111); int er2 = body.add_edge(v111, v101); int er3 = body.add_edge(v101, v100); int lr = body.add_loop({er0, er1, er2, er3}, true); auto surf = make_test_plane(size, size); int s0 = body.add_surface(surf); int s1 = body.add_surface(surf); int s2 = body.add_surface(surf); int s3 = body.add_surface(surf); int s4 = body.add_surface(surf); int s5 = body.add_surface(surf); body.add_face(s0, {lb}); body.add_face(s1, {lt}); body.add_face(s2, {lf}); body.add_face(s3, {lk}); body.add_face(s4, {ll}); body.add_face(s5, {lr}); int shell = body.add_shell({0, 1, 2, 3, 4, 5}, true); body.add_body({shell}, "Cube"); return body; } // ═══════════════════════════════════════════════════════════ // Euler-Poincaré // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, EulerPoincare_Cube) { auto box = make_cube(10.0); int ep = EulerOp::euler_poincare(box); EXPECT_EQ(ep, 2); } TEST(EulerOpTest, VerifyEuler_Cube) { auto box = make_cube(10.0); EXPECT_TRUE(EulerOp::verify_euler(box)); } // ═══════════════════════════════════════════════════════════ // MEV // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, MEV_SplitsEdge) { auto body = make_cube(10.0); auto result = EulerOp::mev(body, 0, 0.5); ASSERT_TRUE(result.success) << result.error; EXPECT_GE(result.new_vertex, 0); EXPECT_GE(result.new_edge, 0); EXPECT_GE(result.new_edge_2, 0); auto& vnew = body.vertex(result.new_vertex); EXPECT_NEAR(vnew.point.y(), -5.0, 1e-6); EXPECT_NEAR(vnew.point.z(), -5.0, 1e-6); EXPECT_NEAR(vnew.point.x(), 0.0, 1e-6); } TEST(EulerOpTest, MEV_AtQuarterParameter) { auto body = make_cube(10.0); auto result = EulerOp::mev(body, 0, 0.25); ASSERT_TRUE(result.success); auto& vnew = body.vertex(result.new_vertex); EXPECT_NEAR(vnew.point.x(), -2.5, 1e-6); } TEST(EulerOpTest, MEV_RejectsBoundaryT) { auto body = make_cube(10.0); EXPECT_FALSE(EulerOp::mev(body, 0, 0.0).success); EXPECT_FALSE(EulerOp::mev(body, 0, 1.0).success); } TEST(EulerOpTest, MEV_ModelRemainsValid) { auto body = make_cube(10.0); auto result = EulerOp::mev(body, 0, 0.5); ASSERT_TRUE(result.success); EXPECT_TRUE(body.is_valid()); } // ═══════════════════════════════════════════════════════════ // KEV // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, KEV_MergesAfterMEV) { auto body = make_cube(10.0); auto mev_r = EulerOp::mev(body, 0, 0.5); ASSERT_TRUE(mev_r.success); auto kev_r = EulerOp::kev(body, mev_r.new_vertex); EXPECT_TRUE(kev_r.success) << kev_r.error; EXPECT_GE(kev_r.new_edge, 0); EXPECT_EQ(kev_r.deleted_vertex, mev_r.new_vertex); } TEST(EulerOpTest, KEV_RejectsNonDegree2) { auto body = make_cube(10.0); auto result = EulerOp::kev(body, 0); EXPECT_FALSE(result.success); } TEST(EulerOpTest, KEV_RejectsNonCollinear) { BrepModel body; int va = body.add_vertex({0, 0, 0}); int vb = body.add_vertex({1, 0, 0}); int vc = body.add_vertex({1, 1, 0}); body.add_edge(va, vb); body.add_edge(vb, vc); // No face/shell — KEV should still detect non-collinearity auto result = EulerOp::kev(body, vb); EXPECT_FALSE(result.success); } // ═══════════════════════════════════════════════════════════ // MEF // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, MEF_SplitsFace) { auto body = make_cube(10.0); // Bottom face vertices: v000=0, v100=4, v101=5, v001=1 auto result = EulerOp::mef(body, 0, 0, 5); ASSERT_TRUE(result.success) << result.error; EXPECT_GE(result.new_edge, 0); EXPECT_GE(result.new_face, 0); EXPECT_GE(result.new_face_2, 0); auto& e = body.edge(result.new_edge); EXPECT_TRUE((e.v_start == 0 && e.v_end == 5) || (e.v_start == 5 && e.v_end == 0)); } TEST(EulerOpTest, MEF_RejectsSameVertex) { auto body = make_cube(10.0); EXPECT_FALSE(EulerOp::mef(body, 0, 0, 0).success); } TEST(EulerOpTest, MEF_RejectsVerticesNotInFace) { auto body = make_cube(10.0); // v000=0 is in bottom face, v010=2 is in left/top faces, not bottom auto result = EulerOp::mef(body, 0, 0, 2); EXPECT_FALSE(result.success); } // ═══════════════════════════════════════════════════════════ // KEF // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, KEF_MergesSplitFaces) { auto body = make_cube(10.0); // MEF to split bottom face auto mef_r = EulerOp::mef(body, 0, 0, 5); ASSERT_TRUE(mef_r.success) << mef_r.error; // KEF to merge them back auto kef_r = EulerOp::kef(body, mef_r.new_edge); EXPECT_TRUE(kef_r.success) << kef_r.error; EXPECT_GE(kef_r.new_face, 0); } TEST(EulerOpTest, KEF_DoesNotCrash) { auto body = make_cube(10.0); auto result = EulerOp::kef(body, 0); // May fail but shouldn't crash SUCCEED(); } // ═══════════════════════════════════════════════════════════ // KEMR / MEKR // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, KEMR_RejectsBoundaryEdge) { auto body = make_cube(10.0); auto result = EulerOp::kemr(body, 0); EXPECT_FALSE(result.success); } TEST(EulerOpTest, VertexDegree_Helper) { auto body = make_cube(10.0); int deg = EulerOp::vertex_degree(body, 0); EXPECT_GT(deg, 0); } // ═══════════════════════════════════════════════════════════ // Euler-Poincaré invariant // ═══════════════════════════════════════════════════════════ TEST(EulerOpTest, EulerPoincare_AfterMEV) { auto body = make_cube(10.0); int ep_before = EulerOp::euler_poincare(body); auto result = EulerOp::mev(body, 0, 0.5); ASSERT_TRUE(result.success); int ep_after = EulerOp::euler_poincare(body); EXPECT_EQ(ep_after, ep_before); } TEST(EulerOpTest, EulerPoincare_MEV_KEV_Roundtrip) { auto body = make_cube(10.0); auto mev_r = EulerOp::mev(body, 0, 0.5); ASSERT_TRUE(mev_r.success); auto kev_r = EulerOp::kev(body, mev_r.new_vertex); ASSERT_TRUE(kev_r.success); EXPECT_GE(kev_r.new_edge, 0); EXPECT_TRUE(body.is_valid()); } TEST(EulerOpTest, EulerPoincare_MEF_KEF_Roundtrip) { auto body = make_cube(10.0); int ep_orig = EulerOp::euler_poincare(body); auto mef_r = EulerOp::mef(body, 0, 0, 5); ASSERT_TRUE(mef_r.success) << mef_r.error; auto kef_r = EulerOp::kef(body, mef_r.new_edge); ASSERT_TRUE(kef_r.success) << kef_r.error; int ep_final = EulerOp::euler_poincare(body); EXPECT_EQ(ep_final, ep_orig); }