fcf25e561d
v4.1 收尾: - IncrementalUpdateEngine: dirty flag propagation, cache invalidation - LargeAssembly: InstanceCache, assembly instancing - STEP import: robust/graceful parsing with skip tracking v4.3 分析工具: - Mass properties (volume, centroid, inertia tensor) - Clearance analysis, wall thickness analysis - Enhanced drawing: hidden-line removal, offset sections, BOM - DXF import (LINE/CIRCLE/ARC/LWPOLYLINE/SPLINE → B-Rep extrusion) v4.4 地基加固: - ToleranceChain: RSS cumulative tolerance propagation (7 tests) - Euler operations: MEV/KEV/MEF/KEF/KEMR/MEKR (20 tests) - Replace hardcoded tolerances with ToleranceConfig in validate - Fix incremental_update test API mismatch (15/15 pass on Linux) Docs: - v4.1-v4.4 development plans + roadmap updated - v4.4 marked complete on Linux 30 files, +3424/-210
299 lines
11 KiB
C++
299 lines
11 KiB
C++
#include <gtest/gtest.h>
|
|
#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<std::vector<Point3D>> 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);
|
|
}
|