#include #include "vde/brep/assembly_patterns.h" #include "vde/brep/assembly.h" #include "vde/brep/modeling.h" #include "vde/core/transform.h" #include using namespace vde::brep; using namespace vde::core; #ifndef M_PI #define M_PI 3.14159265358979323846 #endif // =========================================================================== // Helpers // =========================================================================== static Transform3D make_test_transform(double x = 0, double y = 0, double z = 0) { return translate(x, y, z); } static Assembly make_test_assembly(const std::string& name = "TestAssembly") { Assembly assembly(name); auto box = make_box(10, 10, 5); assembly.root.add_part("SourcePart", std::move(box), make_test_transform(0, 0, 0)); return assembly; } // =========================================================================== // circular_pattern 测试 // =========================================================================== TEST(AssemblyPatternsTest, CircularPattern_FullCircle) { Transform3D src = make_test_transform(30, 0, 0); // 在 X=30 处 CircularPatternParams params; params.center = Point3D(0, 0, 0); params.axis = Vector3D::UnitZ(); params.count = 8; params.total_angle_deg = 360.0; auto result = circular_pattern(src, "TestPart", params); EXPECT_EQ(result.instances.size(), 8u); EXPECT_EQ(result.total_count, 8u); EXPECT_EQ(result.skipped_count, 0u); // 所有实例应距离圆心 30mm for (const auto& inst : result.instances) { Point3D pos(inst.transform.translation().x(), inst.transform.translation().y(), inst.transform.translation().z()); double dist = (pos - params.center).norm(); EXPECT_NEAR(dist, 30.0, 1e-6); } } TEST(AssemblyPatternsTest, CircularPattern_PartialArc) { Transform3D src = make_test_transform(20, 0, 0); CircularPatternParams params; params.center = Point3D(0, 0, 0); params.axis = Vector3D::UnitZ(); params.count = 4; params.total_angle_deg = 180.0; auto result = circular_pattern(src, "ArcPart", params); EXPECT_EQ(result.instances.size(), 4u); } TEST(AssemblyPatternsTest, CircularPattern_SkipInstances) { Transform3D src = make_test_transform(25, 0, 0); CircularPatternParams params; params.center = Point3D(0, 0, 0); params.axis = Vector3D::UnitZ(); params.count = 6; params.skip_instances = {1, 3, 5}; auto result = circular_pattern(src, "SkipPart", params); EXPECT_EQ(result.instances.size(), 3u); EXPECT_EQ(result.skipped_count, 3u); } // =========================================================================== // rectangular_pattern 测试 // =========================================================================== TEST(AssemblyPatternsTest, RectangularPattern_BasicGrid) { Transform3D src = make_test_transform(0, 0, 0); RectangularPatternParams params; params.direction1 = Vector3D::UnitX(); params.direction2 = Vector3D::UnitY(); params.spacing1 = 15.0; params.spacing2 = 12.0; params.count1 = 3; params.count2 = 4; auto result = rectangular_pattern(src, "GridPart", params); EXPECT_EQ(result.instances.size(), 12u); // 3 × 4 EXPECT_EQ(result.total_count, 12u); // 第一个实例应为原点 Point3D p0(result.instances[0].transform.translation().x(), result.instances[0].transform.translation().y(), result.instances[0].transform.translation().z()); EXPECT_NEAR(p0.x(), 0.0, 1e-9); EXPECT_NEAR(p0.y(), 0.0, 1e-9); } TEST(AssemblyPatternsTest, RectangularPattern_Staggered) { Transform3D src = make_test_transform(0, 0, 0); RectangularPatternParams params; params.direction1 = Vector3D::UnitX(); params.direction2 = Vector3D::UnitY(); params.spacing1 = 20.0; params.spacing2 = 20.0; params.count1 = 3; params.count2 = 3; params.staggered = true; params.stagger_offset = 0.5; auto result = rectangular_pattern(src, "StaggerPart", params); EXPECT_EQ(result.instances.size(), 9u); // 3 × 3 // 检查奇数行(索引 i=1 即第二行)有偏移 // 第 4 个实例 (i=1, j=0): index=3 Point3D p3(result.instances[3].transform.translation().x(), result.instances[3].transform.translation().y(), result.instances[3].transform.translation().z()); EXPECT_NEAR(p3.x(), 20.0, 1e-9); // spacing1 * 1 EXPECT_NEAR(p3.y(), 10.0, 1e-9); // stagger offset = 20 * 0.5 } // =========================================================================== // mirror_pattern 测试 // =========================================================================== TEST(AssemblyPatternsTest, MirrorPattern_WithOriginal) { Transform3D src = make_test_transform(10, 0, 0); // X=10 MirrorPatternParams params; params.plane_origin = Point3D(0, 0, 0); params.plane_normal = Vector3D::UnitY(); // YZ 平面镜像 params.copy_original = true; auto result = mirror_pattern(src, "MirrorPart", params); EXPECT_EQ(result.instances.size(), 2u); // 源 + 镜像 EXPECT_EQ(result.total_count, 2u); // 源应在 X=10 Point3D p0(result.instances[0].transform.translation().x(), result.instances[0].transform.translation().y(), result.instances[0].transform.translation().z()); EXPECT_NEAR(p0.x(), 10.0, 1e-9); EXPECT_NEAR(p0.y(), 0.0, 1e-9); // 镜像应在 X=10, Y 不变(YZ平面镜像 → X不变, Y不变, Z不变?不,是平面法向=Y // 实际上:镜像面是 XZ 平面 (法向 Y),所以 Y 坐标取反 Point3D p1(result.instances[1].transform.translation().x(), result.instances[1].transform.translation().y(), result.instances[1].transform.translation().z()); EXPECT_NEAR(p1.y(), 0.0, 1e-9); // 源在 XZ 平面上(Y=0),所以不变 } TEST(AssemblyPatternsTest, MirrorPattern_NoOriginal) { Transform3D src = make_test_transform(0, 5, 0); MirrorPatternParams params; params.plane_origin = Point3D(0, 0, 0); params.plane_normal = Vector3D::UnitX(); // YZ 平面镜像 params.copy_original = false; auto result = mirror_pattern(src, "MirrorOnly", params); EXPECT_EQ(result.instances.size(), 1u); // 仅镜像 EXPECT_EQ(result.total_count, 1u); Point3D p0(result.instances[0].transform.translation().x(), result.instances[0].transform.translation().y(), result.instances[0].transform.translation().z()); EXPECT_NEAR(p0.x(), 0.0, 1e-9); // 源在 YZ 平面上(X=0) → 镜像仍在 X=0 } // =========================================================================== // pattern_driven 测试 // =========================================================================== TEST(AssemblyPatternsTest, PatternDriven_WithoutSourceAssembly) { Transform3D src = make_test_transform(0, 0, 0); PatternDrivenParams params; params.feature_type = PatternDrivenParams::FeatureType::Hole; auto result = pattern_driven(src, "DrivenPart", params); // 无源装配体时,使用模拟特征位置 EXPECT_GT(result.instances.size(), 0u); } TEST(AssemblyPatternsTest, PatternDriven_MaxInstancesLimit) { Transform3D src = make_test_transform(0, 0, 0); PatternDrivenParams params; params.max_instances = 3; auto result = pattern_driven(src, "LimitedPart", params); EXPECT_LE(result.instances.size(), static_cast(params.max_instances)); } // =========================================================================== // fill_pattern 测试 // =========================================================================== TEST(AssemblyPatternsTest, FillPattern_SquareGrid) { Transform3D src = make_test_transform(0, 0, 0); FillPatternParams params; params.fill_min = Point3D(0, 0, 0); params.fill_max = Point3D(50, 50, 0); params.spacing = 10.0; params.margin = 5.0; params.hexagonal = false; auto result = fill_pattern(src, "FillPart", params); // 50x50 区域,间距 10mm,边距 5mm → 约 4x4=16 个实例 EXPECT_GT(result.instances.size(), 10u); EXPECT_LT(result.instances.size(), 30u); } TEST(AssemblyPatternsTest, FillPattern_HexagonalGrid) { Transform3D src = make_test_transform(0, 0, 0); FillPatternParams params; params.fill_min = Point3D(0, 0, 0); params.fill_max = Point3D(40, 40, 0); params.spacing = 10.0; params.margin = 5.0; params.hexagonal = true; auto result = fill_pattern(src, "HexFillPart", params); EXPECT_GT(result.instances.size(), 5u); } TEST(AssemblyPatternsTest, FillPattern_ExcludeRegions) { Transform3D src = make_test_transform(0, 0, 0); FillPatternParams params; params.fill_min = Point3D(0, 0, 0); params.fill_max = Point3D(50, 50, 0); params.spacing = 10.0; params.margin = 2.0; // 排除中心区域 params.exclude_regions = { {Point3D(20, 20, -1), Point3D(30, 30, 1)} }; auto result_no_exclude = fill_pattern(src, "NoExclude", FillPatternParams{}); auto result_with_exclude = fill_pattern(src, "WithExclude", params); // 带排除区的应少于不带排除区的 EXPECT_LT(result_with_exclude.instances.size(), result_no_exclude.instances.size()); EXPECT_GT(result_with_exclude.skipped_count, 0u); } // =========================================================================== // apply_pattern_to_assembly 测试 // =========================================================================== TEST(AssemblyPatternsTest, ApplyPatternToAssembly_AddsNodes) { Assembly assembly("PatternAssembly"); Transform3D src = make_test_transform(0, 0, 0); CircularPatternParams params; params.center = Point3D(0, 0, 0); params.axis = Vector3D::UnitZ(); params.count = 4; params.total_angle_deg = 360.0; auto result = circular_pattern(src, "RingPart", params); size_t before = assembly.node_count(); size_t applied = apply_pattern_to_assembly(assembly, result, "RingPart"); size_t after = assembly.node_count(); EXPECT_EQ(applied, 4u); EXPECT_EQ(after, before + applied); } // =========================================================================== // assembly_feature 增强功能测试 (PMI + 干涉检查) // =========================================================================== TEST(AssemblyPatternsTest, PMIPropagation_ResolvesAnnotations) { Assembly assembly("PMITest"); auto box = make_box(10, 10, 5); assembly.root.add_part("Bracket", std::move(box), make_test_transform(10, 0, 0)); std::vector annotations; PMIAnnotation anno; anno.id = "PMI_001"; anno.type = PMIType::Dimension; anno.label = "Hole spacing: 25mm"; anno.target_feature_id = "Bracket"; anno.position = Point3D(5, 5, 5); // 零件坐标系 annotations.push_back(anno); auto result = propagate_pmi_to_assembly(assembly, annotations); EXPECT_EQ(result.total_source_count, 1u); EXPECT_EQ(result.propagated_count, 1u); EXPECT_EQ(result.unresolved_count, 0u); // 应该变换到世界坐标 EXPECT_GT(result.propagated_annotations[0].position.x(), 5.0); } TEST(AssemblyPatternsTest, InterferenceCheckBatch_NoInterference) { Assembly assembly("ClearanceAssembly"); auto box1 = make_box(5, 5, 3); auto box2 = make_box(5, 5, 3); assembly.root.add_part("Part_A", std::move(box1), make_test_transform(0, 0, 0)); assembly.root.add_part("Part_B", std::move(box2), make_test_transform(20, 0, 0)); // 远离 auto result = interference_check_batch(assembly); EXPECT_GT(result.total_pairs_checked, 0u); EXPECT_EQ(result.interference_count, 0u) << "Well-separated parts should not interfere"; EXPECT_FALSE(result.summary_report.empty()); }