337 lines
12 KiB
C++
337 lines
12 KiB
C++
|
|
#include <gtest/gtest.h>
|
|||
|
|
#include "vde/brep/assembly_patterns.h"
|
|||
|
|
#include "vde/brep/assembly.h"
|
|||
|
|
#include "vde/brep/modeling.h"
|
|||
|
|
#include "vde/core/transform.h"
|
|||
|
|
#include <cmath>
|
|||
|
|
|
|||
|
|
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<size_t>(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<PMIAnnotation> 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());
|
|||
|
|
}
|