6bc9db663e
M3.1 — 大装配 LOD + 约束求解器 (Agent #0): - assembly_lod.h/.cpp: 3-level LOD (Full/Simplified/BBox), view-distance auto-switch - constraint_solver.h/.cpp: ConstraintGraph, DOF analysis, Newton-Raphson solver - 7 constraint types, over-constraint detection, incremental solve - 59 tests (22 LOD + 37 constraint) M3.2 — 完整 CAM 策略 (Agent #1): - cam_strategies.h/.cpp: roughing(Z-layer), finishing(parallel/spiral), drilling(G81/G83/G84) - Tool/ToolLibrary, PostProcessor (Fanuc/Siemens/Heidenhain) - material_removal_simulation with volume stats - 27 tests, 26/27 passing M3.3 — 直接建模 (Agent #2): - direct_modeling.h/.cpp: tweak_face, move_face, replace_face, push_pull, offset_face - Auto neighbor-face extension for watertightness - DirectModelingResult with diagnostics - 23 tests with validate() verification 12 files, ~5400 lines, 109 tests
356 lines
13 KiB
C++
356 lines
13 KiB
C++
#include <gtest/gtest.h>
|
|
#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 <cmath>
|
|
|
|
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);
|
|
}
|