feat(v4.4): complete remaining v4.1-v4.4 features + precision tolerance + Euler ops
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
This commit is contained in:
@@ -22,3 +22,4 @@ add_vde_test(test_gdt)
|
||||
add_vde_test(test_incremental_update)
|
||||
add_vde_test(test_v4_1)
|
||||
add_vde_test(test_tolerance)
|
||||
add_vde_test(test_euler_op)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep_drawing.h"
|
||||
#include "vde/brep/dxf_import.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
@@ -271,3 +272,322 @@ TEST(OffsetSectionTest, CylinderSection) {
|
||||
auto view = offset_section_view(cyl, Vector3D(0, 0, 1), {-2.0, 0.0, 2.0});
|
||||
EXPECT_GE(view.total_segments(), 3u);
|
||||
}
|
||||
|
||||
|
||||
TEST(DxfImportTest, EmptyString) {
|
||||
auto result = import_dxf_from_string("");
|
||||
EXPECT_EQ(result.entities_parsed, 0);
|
||||
EXPECT_EQ(result.contours.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, MinimalDxfWithLine) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
5.0
|
||||
21
|
||||
5.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
EXPECT_GE(result.contours.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, MinimalDxfWithCircle) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
CIRCLE
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
3.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
EXPECT_GE(result.contours.size(), 1u);
|
||||
// Circle should be closed
|
||||
EXPECT_TRUE(result.contours[0].closed);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, MinimalDxfWithArc) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
ARC
|
||||
8
|
||||
0
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
2.0
|
||||
50
|
||||
0.0
|
||||
51
|
||||
180.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
EXPECT_GE(result.contours.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, LwPolylineRectangle) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LWPOLYLINE
|
||||
8
|
||||
walls
|
||||
90
|
||||
4
|
||||
70
|
||||
1
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
10
|
||||
10.0
|
||||
20
|
||||
0.0
|
||||
10
|
||||
10.0
|
||||
20
|
||||
5.0
|
||||
10
|
||||
0.0
|
||||
20
|
||||
5.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
// Find the LWPOLYLINE contour
|
||||
bool found_rect = false;
|
||||
for (const auto& c : result.contours) {
|
||||
if (c.layer == "walls" && c.points.size() >= 4) {
|
||||
found_rect = true;
|
||||
EXPECT_TRUE(c.closed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(found_rect);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, SplineEntity) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
SPLINE
|
||||
8
|
||||
curves
|
||||
70
|
||||
8
|
||||
71
|
||||
3
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
10
|
||||
2.0
|
||||
20
|
||||
3.0
|
||||
30
|
||||
0.0
|
||||
10
|
||||
4.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
10
|
||||
6.0
|
||||
20
|
||||
3.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
0.0
|
||||
40
|
||||
0.0
|
||||
40
|
||||
0.0
|
||||
40
|
||||
0.0
|
||||
40
|
||||
0.333
|
||||
40
|
||||
0.667
|
||||
40
|
||||
1.0
|
||||
40
|
||||
1.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, UnknownEntityTypeIsSkipped) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
MYSTERY_ENTITY
|
||||
8
|
||||
0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0
|
||||
20
|
||||
0
|
||||
30
|
||||
0
|
||||
11
|
||||
1
|
||||
21
|
||||
1
|
||||
31
|
||||
0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 1);
|
||||
EXPECT_GE(result.entities_skipped, 1);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, MultipleEntities) {
|
||||
std::string dxf = R"(0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
0
|
||||
20
|
||||
0
|
||||
30
|
||||
0
|
||||
11
|
||||
1
|
||||
21
|
||||
0
|
||||
31
|
||||
0
|
||||
0
|
||||
CIRCLE
|
||||
8
|
||||
holes
|
||||
10
|
||||
5
|
||||
20
|
||||
5
|
||||
30
|
||||
0
|
||||
40
|
||||
3.0
|
||||
0
|
||||
ARC
|
||||
8
|
||||
holes
|
||||
10
|
||||
0
|
||||
20
|
||||
0
|
||||
30
|
||||
0
|
||||
40
|
||||
2.5
|
||||
50
|
||||
90
|
||||
51
|
||||
270
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
)";
|
||||
auto result = import_dxf_from_string(dxf);
|
||||
EXPECT_GE(result.entities_parsed, 3);
|
||||
EXPECT_GE(result.contours.size(), 3u);
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, ExtrudeContour_ProducesValidBody) {
|
||||
DxfContour c;
|
||||
c.points = {Point3D(0, 0, 0), Point3D(10, 0, 0), Point3D(10, 5, 0), Point3D(0, 5, 0)};
|
||||
c.closed = true;
|
||||
c.layer = "profile";
|
||||
|
||||
auto body = extrude_dxf_contour(c, 2.0);
|
||||
EXPECT_GT(body.num_faces(), 0u);
|
||||
EXPECT_TRUE(body.is_valid());
|
||||
}
|
||||
|
||||
TEST(DxfImportTest, ExtrudeEmptyContour_ReturnsEmpty) {
|
||||
DxfContour c;
|
||||
auto body = extrude_dxf_contour(c, 1.0);
|
||||
EXPECT_EQ(body.num_faces(), 0u);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
#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);
|
||||
}
|
||||
@@ -600,3 +600,83 @@ TEST(StepImport, ShellBasedSurfaceModel) {
|
||||
EXPECT_TRUE(bodies[0].is_valid());
|
||||
EXPECT_GE(bodies[0].num_faces(), 1u);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Fault tolerance tests
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(StepImport, UnknownEntityTypeIsSkipped) {
|
||||
// UNKNOWN_CURVE_TYPE is not recognized → should be skipped, not crash
|
||||
std::string step = step_header() + R"(
|
||||
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#2=DIRECTION('',(0.0,0.0,1.0));
|
||||
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
|
||||
#4=CARTESIAN_POINT('',(5.0,0.0,0.0));
|
||||
#5=VERTEX_POINT('',#4);
|
||||
#6=CIRCLE('',#3,5.0);
|
||||
#7=EDGE_CURVE('',#5,#5,#6,.T.);
|
||||
#8=ORIENTED_EDGE('',*,*,#7,.T.);
|
||||
#9=EDGE_LOOP('',(#8));
|
||||
#10=FACE_OUTER_BOUND('',#9,.T.);
|
||||
#11=PLANE('',#3);
|
||||
#12=ADVANCED_FACE('',(#10),#11,.T.);
|
||||
#13=CLOSED_SHELL('',(#12));
|
||||
#14=MANIFOLD_SOLID_BREP('good_solid',#13);
|
||||
#15=UNKNOWN_CURVE_TYPE('weird',(1.0,2.0,3.0));
|
||||
)" + step_footer();
|
||||
|
||||
auto bodies = import_step_from_string(step);
|
||||
// Should still import the valid solid
|
||||
ASSERT_GE(bodies.size(), 1u);
|
||||
EXPECT_TRUE(bodies[0].is_valid());
|
||||
}
|
||||
|
||||
TEST(StepImport, MissingEntityReferenceIsTolerated) {
|
||||
// #999 doesn't exist → converter should handle gracefully
|
||||
std::string step = step_header() + R"(
|
||||
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#2=DIRECTION('',(0.0,0.0,1.0));
|
||||
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
|
||||
#4=CARTESIAN_POINT('',(5.0,0.0,0.0));
|
||||
#5=VERTEX_POINT('',#4);
|
||||
#6=CIRCLE('',#3,5.0);
|
||||
#7=EDGE_CURVE('',#5,#5,#6,.T.);
|
||||
#8=ORIENTED_EDGE('',*,*,#7,.T.);
|
||||
#9=EDGE_LOOP('',(#8));
|
||||
#10=FACE_OUTER_BOUND('',#9,.T.);
|
||||
#11=PLANE('',#3);
|
||||
#12=ADVANCED_FACE('',(#10),#11,.T.);
|
||||
#13=CLOSED_SHELL('',(#12));
|
||||
#14=MANIFOLD_SOLID_BREP('valid_one',#13);
|
||||
#15=MANIFOLD_SOLID_BREP('broken_one',#999);
|
||||
)" + step_footer();
|
||||
|
||||
auto bodies = import_step_from_string(step);
|
||||
// The valid solid should still be imported
|
||||
ASSERT_GE(bodies.size(), 1u);
|
||||
}
|
||||
|
||||
TEST(StepImport, MixedValidAndInvalidEntities) {
|
||||
// Mix valid and invalid entities → valid parts are imported
|
||||
std::string step = step_header() + R"(
|
||||
#1=CARTESIAN_POINT('',(0.0,0.0,0.0));
|
||||
#2=DIRECTION('',(0.0,0.0,1.0));
|
||||
#3=AXIS2_PLACEMENT_3D('',#1,#2,$);
|
||||
#4=CARTESIAN_POINT('',(5.0,0.0,0.0));
|
||||
#5=VERTEX_POINT('',#4);
|
||||
#6=CIRCLE('',#3,5.0);
|
||||
#7=EDGE_CURVE('',#5,#5,#6,.T.);
|
||||
#8=ORIENTED_EDGE('',*,*,#7,.T.);
|
||||
#9=EDGE_LOOP('',(#8));
|
||||
#10=FACE_OUTER_BOUND('',#9,.T.);
|
||||
#11=PLANE('',#3);
|
||||
#12=ADVANCED_FACE('',(#10),#11,.T.);
|
||||
#13=CLOSED_SHELL('',(#12));
|
||||
#14=MANIFOLD_SOLID_BREP('good',#13);
|
||||
#1000=WEIRD_CURVE_TYPE('odd',(1.0,2.0));
|
||||
)" + step_footer();
|
||||
|
||||
auto bodies = import_step_from_string(step);
|
||||
ASSERT_GE(bodies.size(), 1u);
|
||||
EXPECT_TRUE(bodies[0].is_valid());
|
||||
}
|
||||
|
||||
@@ -91,3 +91,68 @@ TEST(ToleranceTest, ToleranceConfig_AllDefaultPositive) {
|
||||
EXPECT_GT(cfg.boolean, 0);
|
||||
EXPECT_GT(cfg.angular, 0);
|
||||
}
|
||||
|
||||
// ── ToleranceChain tests ──
|
||||
|
||||
TEST(ToleranceChainTest, EmptyChain) {
|
||||
ToleranceChain chain;
|
||||
EXPECT_EQ(chain.depth(), 0u);
|
||||
EXPECT_EQ(chain.cumulative(), 0.0);
|
||||
EXPECT_EQ(chain.max_step(), 0.0);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, SingleStep) {
|
||||
ToleranceChain chain;
|
||||
chain.push("intersect", 1e-6);
|
||||
EXPECT_EQ(chain.depth(), 1u);
|
||||
EXPECT_DOUBLE_EQ(chain.cumulative(), 1e-6);
|
||||
EXPECT_DOUBLE_EQ(chain.max_step(), 1e-6);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, MultiStepRSS) {
|
||||
ToleranceChain chain;
|
||||
chain.push("a", 3e-6);
|
||||
chain.push("b", 4e-6);
|
||||
// RSS: sqrt(3² + 4²) * 1e-6 = 5e-6
|
||||
EXPECT_DOUBLE_EQ(chain.cumulative(), 5e-6);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, MaxStep) {
|
||||
ToleranceChain chain;
|
||||
chain.push("small", 1e-8);
|
||||
chain.push("large", 1e-4);
|
||||
chain.push("med", 1e-6);
|
||||
EXPECT_DOUBLE_EQ(chain.max_step(), 1e-4);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, ClearResets) {
|
||||
ToleranceChain chain;
|
||||
chain.push("x", 1e-6);
|
||||
chain.clear();
|
||||
EXPECT_EQ(chain.depth(), 0u);
|
||||
EXPECT_EQ(chain.cumulative(), 0.0);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, StepsAccess) {
|
||||
ToleranceChain chain;
|
||||
chain.push("op1", 1e-9);
|
||||
chain.push("op2", 2e-9);
|
||||
auto& steps = chain.steps();
|
||||
ASSERT_EQ(steps.size(), 2u);
|
||||
EXPECT_EQ(steps[0].first, "op1");
|
||||
EXPECT_DOUBLE_EQ(steps[0].second, 1e-9);
|
||||
EXPECT_EQ(steps[1].first, "op2");
|
||||
EXPECT_DOUBLE_EQ(steps[1].second, 2e-9);
|
||||
}
|
||||
|
||||
TEST(ToleranceChainTest, BooleanChainSimulation) {
|
||||
ToleranceChain chain;
|
||||
chain.push("intersect", 1e-6);
|
||||
chain.push("split", 1e-6);
|
||||
chain.push("classify", 1e-7);
|
||||
chain.push("sew", 1e-5);
|
||||
// Cumulative should be dominated by the sewer step
|
||||
double cum = chain.cumulative();
|
||||
EXPECT_GT(cum, 1e-5);
|
||||
EXPECT_LT(cum, 1.5e-5);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user