#include #include "vde/brep/assembly_lod.h" #include "vde/brep/modeling.h" #include "vde/brep/assembly_instance.h" #include "vde/core/transform.h" #include "vde/core/aabb.h" #include using namespace vde::brep; using namespace vde::core; // ═══════════════════════════════════════════════════════════ // 辅助函数 // ═══════════════════════════════════════════════════════════ /// 创建多零件测试装配体 static Assembly make_test_assembly() { Assembly assy("test_lod"); assy.root.add_part("box_a", make_box(2, 2, 2)); assy.root.add_part("box_b", make_box(2, 2, 2), translate(5, 0, 0)); assy.root.add_part("box_c", make_box(2, 2, 2), translate(10, 0, 0)); return assy; } // ═══════════════════════════════════════════════════════════ // 测试 1: 构造与基本属性 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, ConstructFromAssembly) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); EXPECT_EQ(lod.part_count(), 3); EXPECT_EQ(lod.nodes().size(), 3); } TEST(AssemblyLODTest, DefaultLODLevelIsFull) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::Full); EXPECT_FALSE(node.manual_override); } } TEST(AssemblyLODTest, HasValidWorldBounds) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); for (const auto& node : lod.nodes()) { // 未初始化(全零)的 AABB 体积为 0,非空 AABB 体积 > 0 EXPECT_GT(node.world_bounds.volume(), 0.0); } } // ═══════════════════════════════════════════════════════════ // 测试 2: 视距驱动 LOD 选择 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, NearPlaneSelectsFull) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 1000.0; lod.set_config(cfg); // camera_distance = 10 < near_plane → Full lod.compute_lod(10.0, 0.5); for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::Full); } } TEST(AssemblyLODTest, FarPlaneSelectsBoundingBox) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 1000.0; lod.set_config(cfg); // camera_distance = 2000 > far_plane → BoundingBox lod.compute_lod(2000.0, 0.1); for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::BoundingBox); } } TEST(AssemblyLODTest, MidRangeWithLargeScreenSizeSelectsSimplified) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 1000.0; lod.set_config(cfg); // near_plane ≤ distance ≤ far_plane, large screen_size → Simplified lod.compute_lod(500.0, 0.5); for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::Simplified); } } TEST(AssemblyLODTest, MidRangeWithSmallScreenSizeSelectsBBox) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 1000.0; cfg.screen_size_threshold = 0.1; lod.set_config(cfg); // near ≤ dist ≤ far, small screen → BoundingBox lod.compute_lod(500.0, 0.001); for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::BoundingBox); } } // ═══════════════════════════════════════════════════════════ // 测试 3: 手动 LOD 设置 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, ManualSetLODLevel) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); auto& node0 = lod.nodes()[0]; lod.set_lod_level(node0, LODLevel::BoundingBox); EXPECT_EQ(node0.current_level, LODLevel::BoundingBox); EXPECT_TRUE(node0.manual_override); } TEST(AssemblyLODTest, ManualOverridePreventsAutoLOD) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); // 手动设置 node0 为 BoundingBox lod.set_lod_level(lod.nodes()[0], LODLevel::BoundingBox); // 自动 LOD 应设置为 Full (near plane) LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 1000.0; lod.set_config(cfg); lod.compute_lod(10.0, 0.5); // near plane → Full // 手动覆盖的节点仍保持 BoundingBox EXPECT_EQ(lod.nodes()[0].current_level, LODLevel::BoundingBox); // 其余节点应该是 Full EXPECT_EQ(lod.nodes()[1].current_level, LODLevel::Full); EXPECT_EQ(lod.nodes()[2].current_level, LODLevel::Full); } TEST(AssemblyLODTest, ClearManualOverride) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); lod.set_lod_level(lod.nodes()[0], LODLevel::Simplified); EXPECT_TRUE(lod.nodes()[0].manual_override); lod.clear_manual_override(lod.nodes()[0]); EXPECT_FALSE(lod.nodes()[0].manual_override); } TEST(AssemblyLODTest, ClearAllManualOverrides) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); lod.set_lod_level(lod.nodes()[0], LODLevel::Simplified); lod.set_lod_level(lod.nodes()[1], LODLevel::BoundingBox); lod.clear_all_manual_overrides(); for (const auto& node : lod.nodes()) { EXPECT_FALSE(node.manual_override); } } // ═══════════════════════════════════════════════════════════ // 测试 4: 遍历与统计 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, TraverseAssemblyVisitsAllNodes) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); int visited = 0; lod.traverse_assembly([&visited](const LODNode&) { ++visited; }); EXPECT_EQ(visited, 3); } TEST(AssemblyLODTest, StatisticsCounts) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); lod.set_lod_level(lod.nodes()[0], LODLevel::Full); lod.set_lod_level(lod.nodes()[1], LODLevel::Simplified); lod.set_lod_level(lod.nodes()[2], LODLevel::BoundingBox); auto stats = lod.statistics(); EXPECT_EQ(stats.full_count, 1); EXPECT_EQ(stats.simplified_count, 1); EXPECT_EQ(stats.bbox_count, 1); EXPECT_EQ(stats.manual_count, 3); // all three are manual } // ═══════════════════════════════════════════════════════════ // 测试 5: compute_lod_for_node 单独使用 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, ComputeLODForNodeNearPlane) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.near_plane = 100.0; cfg.far_plane = 500.0; lod.set_config(cfg); auto level = lod.compute_lod_for_node(lod.nodes()[0], 10.0, 0.5); EXPECT_EQ(level, LODLevel::Full); } TEST(AssemblyLODTest, DisableAutoLODReturnsFull) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); LODConfig cfg; cfg.enable_auto_lod = false; lod.set_config(cfg); lod.compute_lod(5000.0, 0.001); // Disabled auto LOD: all stay Full for (const auto& node : lod.nodes()) { EXPECT_EQ(node.current_level, LODLevel::Full); } } // ═══════════════════════════════════════════════════════════ // 测试 6: 更新边界 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, UpdateBoundsAfterTransformChange) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); auto bb_before = lod.nodes()[0].world_bounds; // 修改 node0 的变换 lod.nodes()[0].node->local_transform = translate(100, 0, 0); lod.update_bounds(); auto bb_after = lod.nodes()[0].world_bounds; // X 坐标应该偏移了 100 EXPECT_NEAR(bb_after.center().x(), bb_before.center().x() + 100.0, 1e-6); } // ═══════════════════════════════════════════════════════════ // 测试 7: 空装配 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, EmptyAssembly) { Assembly assy("empty"); AssemblyLOD lod(assy); EXPECT_EQ(lod.part_count(), 0); EXPECT_EQ(lod.nodes().size(), 0); auto stats = lod.statistics(); EXPECT_EQ(stats.full_count, 0); EXPECT_EQ(stats.simplified_count, 0); EXPECT_EQ(stats.bbox_count, 0); } // ═══════════════════════════════════════════════════════════ // 测试 8: LODConfig 默认值 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, DefaultConfig) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); const auto& cfg = lod.config(); EXPECT_DOUBLE_EQ(cfg.near_plane, 50.0); EXPECT_DOUBLE_EQ(cfg.far_plane, 500.0); EXPECT_TRUE(cfg.enable_auto_lod); } // ═══════════════════════════════════════════════════════════ // 测试 9: traverse_with_context // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, TraverseWithContextCallsAllParts) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); int parts_seen = 0; lod.traverse_with_context([&](LODNode&, const Transform3D&, int) { ++parts_seen; return true; }); EXPECT_EQ(parts_seen, 3); } TEST(AssemblyLODTest, TraverseWithContextCanEarlyExit) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); int parts_seen = 0; bool stopped = lod.traverse_with_context([&](LODNode&, const Transform3D&, int) { ++parts_seen; return false; // stop after first part }); EXPECT_FALSE(stopped); EXPECT_EQ(parts_seen, 1); } // ═══════════════════════════════════════════════════════════ // 测试 10: InstanceCache 集成 // ═══════════════════════════════════════════════════════════ TEST(AssemblyLODTest, InstanceCacheIsPopulated) { auto assy = make_test_assembly(); AssemblyLOD lod(assy); // InstanceCache 应已填充 EXPECT_GT(lod.cache().unique_count(), 0); } TEST(AssemblyLODTest, SubAssemblyDoesNotAddToLOD) { Assembly assy("mixed"); assy.root.add_part("part_a", make_box(1, 1, 1)); auto* sub = assy.root.add_subassembly("sub_a"); sub->add_part("part_b", make_box(1, 1, 1)); AssemblyLOD lod(assy); // 应只有零件节点(part_a 和 part_b),没有子装配体 EXPECT_EQ(lod.part_count(), 2); }