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
594 lines
13 KiB
C++
594 lines
13 KiB
C++
#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>
|
||
#include <cstdio>
|
||
|
||
using namespace vde::brep;
|
||
using namespace vde::core;
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// generate_views — 基本测试
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Box_ReturnsFourViews) {
|
||
auto box = make_box(2, 3, 4);
|
||
auto views = generate_views(box);
|
||
|
||
EXPECT_EQ(views.size(), 4u);
|
||
EXPECT_EQ(views[0].name, "front");
|
||
EXPECT_EQ(views[1].name, "top");
|
||
EXPECT_EQ(views[2].name, "right");
|
||
EXPECT_EQ(views[3].name, "iso");
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Box_EachViewHasSegments) {
|
||
auto box = make_box(2, 3, 4);
|
||
auto views = generate_views(box);
|
||
|
||
for (const auto& v : views) {
|
||
EXPECT_GT(v.total_segments(), 0u)
|
||
<< "View " << v.name << " should have segments";
|
||
}
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Box_FrontViewHasExpectedContent) {
|
||
auto box = make_box(2, 2, 2);
|
||
auto views = generate_views(box);
|
||
|
||
const auto& front = views[0];
|
||
EXPECT_EQ(front.name, "front");
|
||
// Front view: XY plane projection, should show X=[-1,1], Y=[-1,1]
|
||
EXPECT_GT(front.total_segments(), 0u);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Box_ViewDirections) {
|
||
auto box = make_box(1, 1, 1);
|
||
auto views = generate_views(box);
|
||
|
||
// Front: looking down -Z
|
||
EXPECT_NEAR(views[0].view_direction.z(), -1.0, 1e-6);
|
||
|
||
// Top: looking down -Y
|
||
EXPECT_NEAR(views[1].view_direction.y(), -1.0, 1e-6);
|
||
|
||
// Right: looking down -X
|
||
EXPECT_NEAR(views[2].view_direction.x(), -1.0, 1e-6);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_EmptyBody_ReturnsEmpty) {
|
||
BrepModel empty;
|
||
auto views = generate_views(empty);
|
||
EXPECT_TRUE(views.empty());
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Cylinder_ReturnsFourViews) {
|
||
auto cyl = make_cylinder(1.0, 3.0, 16);
|
||
auto views = generate_views(cyl);
|
||
|
||
EXPECT_EQ(views.size(), 4u);
|
||
for (const auto& v : views) {
|
||
EXPECT_GT(v.total_segments(), 0u);
|
||
}
|
||
}
|
||
|
||
TEST(BrepDrawingTest, GenerateViews_Sphere_ReturnsFourViews) {
|
||
auto sphere = make_sphere(2.0, 16, 8);
|
||
auto views = generate_views(sphere);
|
||
|
||
EXPECT_EQ(views.size(), 4u);
|
||
// 球所有视图的主要轮廓应类似
|
||
for (const auto& v : views) {
|
||
ASSERT_GT(v.total_segments(), 0u);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// section_view — 测试
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
TEST(BrepDrawingTest, SectionView_Box_AtCenter) {
|
||
auto box = make_box(4, 4, 4);
|
||
// 截平面 z = 0(XY 平面,法向量沿 Z)
|
||
auto view = section_view(box, Point3D(0, 0, 0), Vector3D(0, 0, 1));
|
||
|
||
EXPECT_EQ(view.name, "section");
|
||
// 截面应为矩形轮廓,至少有 4 个交点形成轮廓线段
|
||
// + hatch 线,总数应 > 4
|
||
ASSERT_GT(view.total_segments(), 4u);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, SectionView_Box_HorizontalPlane) {
|
||
auto box = make_box(4, 4, 4);
|
||
// z = 1 的水平截面(穿过盒子)
|
||
auto view = section_view(box, Point3D(0, 0, 1), Vector3D(0, 0, 1));
|
||
|
||
ASSERT_GT(view.total_segments(), 4u);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, SectionView_Box_PlaneOutside) {
|
||
auto box = make_box(2, 2, 2);
|
||
// 截平面在盒子之外(z = 5)
|
||
auto view = section_view(box, Point3D(0, 0, 5), Vector3D(0, 0, 1));
|
||
|
||
// 交点不足 3 个 → 无法形成截面
|
||
EXPECT_LT(view.total_segments(), 3u);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, SectionView_EmptyBody_ReturnsEmpty) {
|
||
BrepModel empty;
|
||
auto view = section_view(empty, Point3D(0, 0, 0), Vector3D(0, 0, 1));
|
||
|
||
EXPECT_EQ(view.name, "section");
|
||
EXPECT_EQ(view.total_segments(), 0u);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, SectionView_Cylinder_Midplane) {
|
||
auto cyl = make_cylinder(2.0, 6.0, 32);
|
||
// 垂直圆柱的 z=0 截面 → 圆形截面
|
||
auto view = section_view(cyl, Point3D(0, 0, 0), Vector3D(0, 0, 1));
|
||
|
||
ASSERT_GT(view.segments.size(), 3u);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// DXF 导出 — 测试
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
TEST(BrepDrawingTest, ExportDxf_CreatesFile) {
|
||
auto box = make_box(2, 2, 2);
|
||
auto views = generate_views(box);
|
||
|
||
const char* filepath = "test_output.dxf";
|
||
std::remove(filepath); // 清理旧文件
|
||
|
||
bool ok = export_dxf(filepath, views);
|
||
EXPECT_TRUE(ok);
|
||
|
||
// 验证文件存在
|
||
std::ifstream in(filepath);
|
||
EXPECT_TRUE(in.is_open());
|
||
std::string content((std::istreambuf_iterator<char>(in)),
|
||
std::istreambuf_iterator<char>());
|
||
in.close();
|
||
|
||
// 验证关键结构
|
||
EXPECT_NE(content.find("SECTION"), std::string::npos);
|
||
EXPECT_NE(content.find("ENTITIES"), std::string::npos);
|
||
EXPECT_NE(content.find("LINE"), std::string::npos);
|
||
EXPECT_NE(content.find("ENDSEC"), std::string::npos);
|
||
EXPECT_NE(content.find("EOF"), std::string::npos);
|
||
|
||
// 验证有至少一条 LINE 实体
|
||
EXPECT_NE(content.find("LINE"), std::string::npos);
|
||
|
||
std::remove(filepath);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, ExportDxf_HasLayers) {
|
||
auto box = make_box(2, 2, 2);
|
||
auto views = generate_views(box);
|
||
|
||
const char* filepath = "test_layers.dxf";
|
||
std::remove(filepath);
|
||
export_dxf(filepath, views);
|
||
|
||
std::ifstream in(filepath);
|
||
std::string content((std::istreambuf_iterator<char>(in)),
|
||
std::istreambuf_iterator<char>());
|
||
in.close();
|
||
|
||
EXPECT_NE(content.find("visible"), std::string::npos);
|
||
EXPECT_NE(content.find("DASHED"), std::string::npos);
|
||
|
||
std::remove(filepath);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, ExportDxf_EmptyViews_StillWritesSkeleton) {
|
||
std::vector<ProjectionView> empty_views;
|
||
|
||
const char* filepath = "test_empty.dxf";
|
||
std::remove(filepath);
|
||
bool ok = export_dxf(filepath, empty_views);
|
||
EXPECT_TRUE(ok);
|
||
|
||
std::ifstream in(filepath);
|
||
std::string content((std::istreambuf_iterator<char>(in)),
|
||
std::istreambuf_iterator<char>());
|
||
in.close();
|
||
|
||
EXPECT_NE(content.find("SECTION"), std::string::npos);
|
||
EXPECT_NE(content.find("EOF"), std::string::npos);
|
||
|
||
std::remove(filepath);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, ExportDxf_InvalidPath_ReturnsFalse) {
|
||
auto box = make_box(1, 1, 1);
|
||
auto views = generate_views(box);
|
||
bool ok = export_dxf("/nonexistent_dir_xyz/test.dxf", views);
|
||
EXPECT_FALSE(ok);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 隐藏线 — 测试
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
TEST(BrepDrawingTest, HiddenLines_Box_HasHiddenEdges) {
|
||
auto box = make_box(2, 2, 2);
|
||
auto views = generate_views(box);
|
||
|
||
// 每个标准视图均应有隐藏边
|
||
for (size_t i = 0; i < std::min(views.size(), size_t(3)); ++i) {
|
||
// 前/俯/右视图都应该有隐藏线
|
||
EXPECT_GE(views[i].hidden_lines.size() + views[i].segments.size(), 1u);
|
||
}
|
||
}
|
||
|
||
TEST(BrepDrawingTest, DrawSegment_DefaultNotHidden) {
|
||
DrawSegment seg{0, 0, 1, 1};
|
||
EXPECT_FALSE(seg.hidden);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, DrawSegment_Hidden) {
|
||
DrawSegment seg{0, 0, 1, 1, true};
|
||
EXPECT_TRUE(seg.hidden);
|
||
}
|
||
|
||
TEST(BrepDrawingTest, ProjectionView_TotalSegments) {
|
||
ProjectionView v;
|
||
v.segments = {{0,0,1,0}, {1,0,1,1}};
|
||
v.hidden_lines = {{0,1,0,0, true}};
|
||
EXPECT_EQ(v.total_segments(), 3u);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// Offset (stepped) section views
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
TEST(OffsetSectionTest, SingleOffset) {
|
||
auto box = make_box(4, 4, 4);
|
||
auto view = offset_section_view(box, Vector3D(0, 0, 1), {0.0});
|
||
EXPECT_FALSE(view.segments.empty());
|
||
EXPECT_EQ(view.name, "offset_section");
|
||
}
|
||
|
||
TEST(OffsetSectionTest, MultipleOffsets) {
|
||
auto box = make_box(4, 4, 4);
|
||
auto view = offset_section_view(box, Vector3D(0, 0, 1), {-1.0, 0.0, 1.0});
|
||
EXPECT_GE(view.segments.size(), 3u); // section + step lines
|
||
}
|
||
|
||
TEST(OffsetSectionTest, EmptyOffsets) {
|
||
auto box = make_box(4, 4, 4);
|
||
auto view = offset_section_view(box, Vector3D(0, 0, 1), {});
|
||
EXPECT_TRUE(view.segments.empty());
|
||
}
|
||
|
||
TEST(OffsetSectionTest, CylinderSection) {
|
||
auto cyl = make_cylinder(2, 6, 32);
|
||
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);
|
||
}
|