Files
ViewDesignEngine/tests/brep/test_assembly_lod.cpp
T
茂之钳 6bc9db663e
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 39s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
feat(v5-M3): large assembly LOD + constraint solver + CAM strategies + direct modeling
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
2026-07-26 21:19:38 +08:00

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);
}