feat(v4.1): incremental update + large assembly + format robustness
- IncrementalUpdateEngine: dirty propagation, topological rebuild, cache hit/miss - InstanceCache: shared LOD mesh for identical parts - LargeAssemblyManager: BVH spatial index, view frustum culling, assembly stats - format_io: auto-detect STEP/IGES, batch import with error recovery - 31 tests: incremental (11), large assembly (12), format IO (8)
This commit is contained in:
@@ -19,3 +19,5 @@ add_vde_test(test_interference_check)
|
||||
add_vde_test(test_motion_simulation)
|
||||
add_vde_test(test_explode_view)
|
||||
add_vde_test(test_gdt)
|
||||
add_vde_test(test_incremental_update)
|
||||
add_vde_test(test_v4_1)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/incremental_update.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
TEST(IncrementalUpdateTest, RegisterNode) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
EXPECT_EQ(engine.total_nodes(), 1);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, RegisterWithDeps) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.register_node(2, {1});
|
||||
engine.register_node(3, {1, 2});
|
||||
EXPECT_EQ(engine.total_nodes(), 3);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, MarkDirtyPropagates) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.register_node(2, {1});
|
||||
engine.register_node(3, {2});
|
||||
engine.mark_dirty(1);
|
||||
EXPECT_EQ(engine.dirty_count(), 3);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, MarkDirtyNoUpstream) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.register_node(2, {1});
|
||||
engine.mark_dirty(2);
|
||||
EXPECT_EQ(engine.dirty_count(), 1);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, CacheMissOnDirty) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.mark_dirty(1);
|
||||
EXPECT_TRUE(engine.get_cache(1) == nullptr);
|
||||
EXPECT_EQ(engine.cache_misses(), 1);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, RebuildClearsDirty) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.mark_dirty(1);
|
||||
EXPECT_EQ(engine.dirty_count(), 1);
|
||||
FeatureHistory history;
|
||||
engine.incremental_rebuild(history);
|
||||
EXPECT_EQ(engine.dirty_count(), 0);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, RebuildWithCallback) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.mark_dirty(1);
|
||||
int cb = 0;
|
||||
FeatureHistory history;
|
||||
engine.incremental_rebuild(history, [&](int nid, const BrepModel&) { cb++; });
|
||||
EXPECT_EQ(cb, 1);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, ChainDependencyOrder) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.register_node(2, {1});
|
||||
engine.mark_dirty(1);
|
||||
std::vector<int> order;
|
||||
FeatureHistory history;
|
||||
engine.incremental_rebuild(history, [&](int nid, const BrepModel&) { order.push_back(nid); });
|
||||
ASSERT_GE(order.size(), 2u);
|
||||
EXPECT_LT(std::find(order.begin(),order.end(),1), std::find(order.begin(),order.end(),2));
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, IncrementalVsFull) {
|
||||
IncrementalUpdateEngine engine;
|
||||
for (int i = 1; i <= 10; ++i) {
|
||||
std::vector<int> deps;
|
||||
if (i > 1) deps.push_back(i - 1);
|
||||
engine.register_node(i, deps);
|
||||
}
|
||||
FeatureHistory history;
|
||||
engine.clear_all();
|
||||
int fc = engine.incremental_rebuild(history);
|
||||
EXPECT_EQ(fc, 10);
|
||||
engine.clear_all();
|
||||
engine.incremental_rebuild(history);
|
||||
engine.mark_dirty(5);
|
||||
int ic = engine.incremental_rebuild(history);
|
||||
EXPECT_LT(ic, fc);
|
||||
EXPECT_GT(ic, 0);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, HitRate) {
|
||||
IncrementalUpdateEngine engine;
|
||||
EXPECT_EQ(engine.hit_rate(), 0.0);
|
||||
}
|
||||
|
||||
TEST(IncrementalUpdateTest, ClearAll) {
|
||||
IncrementalUpdateEngine engine;
|
||||
engine.register_node(1);
|
||||
engine.register_node(2);
|
||||
engine.clear_all();
|
||||
EXPECT_EQ(engine.dirty_count(), 2);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "vde/brep/step_export.h"
|
||||
#include "vde/brep/iges_export.h"
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/large_assembly.h"
|
||||
#include "vde/brep/format_io.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// LargeAssemblyManager
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(LargeAssemblyTest, BuildIndex_EmptyAssembly) {
|
||||
Assembly assy("empty");
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
EXPECT_TRUE(mgr.index_built());
|
||||
EXPECT_EQ(mgr.part_count(), 0);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, BuildIndex_SinglePart) {
|
||||
Assembly assy("single");
|
||||
assy.root.add_part("box", make_box(2, 2, 2));
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
EXPECT_EQ(mgr.part_count(), 1);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, BuildIndex_MultipleParts) {
|
||||
Assembly assy("multi");
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
assy.root.add_part("p" + std::to_string(i), make_box(1, 1, 1),
|
||||
translate(i * 3.0, 0, 0));
|
||||
}
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
EXPECT_EQ(mgr.part_count(), 10);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, QueryVisible_ReturnsIntersectingParts) {
|
||||
Assembly assy("visible");
|
||||
assy.root.add_part("near", make_box(2, 2, 2), translate(0, 0, 0));
|
||||
assy.root.add_part("far", make_box(2, 2, 2), translate(20, 0, 0));
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
|
||||
// AABB covering origin region
|
||||
AABB3D view(Point3D(-5, -5, -5), Point3D(5, 5, 5));
|
||||
auto visible = mgr.query_visible(view);
|
||||
|
||||
EXPECT_GE(visible.size(), 1u);
|
||||
// "near" should be visible
|
||||
bool has_near = false;
|
||||
for (auto& n : visible) if (n == "near") has_near = true;
|
||||
EXPECT_TRUE(has_near);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, QueryVisible_FarRegion_ReturnsEmpty) {
|
||||
Assembly assy("far_test");
|
||||
assy.root.add_part("box", make_box(2, 2, 2), translate(0, 0, 0));
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
|
||||
// AABB far away
|
||||
AABB3D view(Point3D(100, 100, 100), Point3D(110, 110, 110));
|
||||
auto visible = mgr.query_visible(view);
|
||||
|
||||
EXPECT_EQ(visible.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, ComputeStats_AccurateCounts) {
|
||||
Assembly assy("stats");
|
||||
assy.root.add_part("p1", make_box(1, 1, 1));
|
||||
assy.root.add_part("p2", make_box(1, 1, 1), translate(5, 0, 0));
|
||||
auto* sub = assy.root.add_subassembly("group");
|
||||
sub->add_part("sub_p1", make_box(1, 1, 1));
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
auto stats = mgr.compute_stats(assy);
|
||||
|
||||
EXPECT_EQ(stats.total_parts, 3);
|
||||
EXPECT_GE(stats.subassemblies, 1);
|
||||
EXPECT_GE(stats.max_depth, 1);
|
||||
}
|
||||
|
||||
TEST(LargeAssemblyTest, LargeAssembly_10KParts_PerformanceTest) {
|
||||
Assembly assy("10k");
|
||||
int grid_size = 100; // 100×100 = 10,000 parts
|
||||
for (int i = 0; i < grid_size; ++i) {
|
||||
for (int j = 0; j < grid_size; ++j) {
|
||||
assy.root.add_part("b_" + std::to_string(i) + "_" + std::to_string(j),
|
||||
make_box(1, 1, 1), translate(i * 3.0, j * 3.0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
LargeAssemblyManager mgr;
|
||||
mgr.build_index(assy);
|
||||
EXPECT_EQ(mgr.part_count(), grid_size * grid_size);
|
||||
|
||||
// Query visible in central region
|
||||
AABB3D view(Point3D(140, 140, -5), Point3D(160, 160, 5));
|
||||
auto visible = mgr.query_visible(view);
|
||||
|
||||
// Should find ~7×7 = 49 parts in this region
|
||||
EXPECT_GT(visible.size(), 0u);
|
||||
EXPECT_LT(visible.size(), 100u);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// InstanceCache
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(InstanceCacheTest, Register_NewInstance_ReturnsTrue) {
|
||||
InstanceCache cache;
|
||||
auto box = make_box(2, 2, 2);
|
||||
EXPECT_TRUE(cache.register_instance(1, box));
|
||||
EXPECT_EQ(cache.unique_count(), 1);
|
||||
}
|
||||
|
||||
TEST(InstanceCacheTest, Register_DuplicateInstance_ReturnsFalse) {
|
||||
InstanceCache cache;
|
||||
auto box = make_box(2, 2, 2);
|
||||
cache.register_instance(1, box);
|
||||
EXPECT_FALSE(cache.register_instance(1, box));
|
||||
EXPECT_EQ(cache.unique_count(), 1);
|
||||
}
|
||||
|
||||
TEST(InstanceCacheTest, GetLod_ReturnsNonNull) {
|
||||
InstanceCache cache;
|
||||
cache.register_instance(1, make_box(2, 2, 2));
|
||||
auto* lod = cache.get_lod(1);
|
||||
EXPECT_TRUE(lod != nullptr);
|
||||
}
|
||||
|
||||
TEST(InstanceCacheTest, GetLod_Unknown_ReturnsNull) {
|
||||
InstanceCache cache;
|
||||
EXPECT_TRUE(cache.get_lod(999) == nullptr);
|
||||
}
|
||||
|
||||
TEST(InstanceCacheTest, GetBounds_ReturnsCorrectSize) {
|
||||
InstanceCache cache;
|
||||
cache.register_instance(1, make_box(2, 2, 2));
|
||||
auto* bb = cache.get_bounds(1);
|
||||
ASSERT_TRUE(bb != nullptr);
|
||||
EXPECT_NEAR(bb->extent().x(), 2.0, 0.01);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Format detection
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(FormatIOTest, Detect_STEP_Header) {
|
||||
auto fmt = detect_format_from_data("ISO-10303-21;\nHEADER;\n...");
|
||||
EXPECT_EQ(fmt, CadFormat::STEP);
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, Detect_IGES_CardFormat) {
|
||||
// 80-column IGES-style data with section marker at column 73
|
||||
std::string data(80, ' ');
|
||||
data[72] = 'S';
|
||||
auto fmt = detect_format_from_data(data);
|
||||
EXPECT_EQ(fmt, CadFormat::IGES);
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, Detect_EmptyData_Unknown) {
|
||||
EXPECT_EQ(detect_format_from_data(""), CadFormat::Unknown);
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, AutoImport_EmptyData_ReturnsEmpty) {
|
||||
auto models = auto_import_from_string("");
|
||||
EXPECT_TRUE(models.empty());
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, BatchImport_EmptyList_ReturnsZero) {
|
||||
auto result = batch_import({});
|
||||
EXPECT_EQ(result.total_files, 0);
|
||||
EXPECT_FALSE(result.summary().empty());
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, BatchImport_InvalidFile_CountsFailure) {
|
||||
auto result = batch_import({"/nonexistent/path.stp"});
|
||||
EXPECT_EQ(result.total_files, 1);
|
||||
EXPECT_GT(result.fail_count + result.success_count, 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP round-trip with format IO
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(FormatIOTest, StepRoundTrip_AutoDetect) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
std::string step = export_step({box});
|
||||
|
||||
auto fmt = detect_format_from_data(step);
|
||||
EXPECT_EQ(fmt, CadFormat::STEP);
|
||||
|
||||
auto models = auto_import_from_string(step);
|
||||
EXPECT_EQ(models.size(), 1u);
|
||||
EXPECT_TRUE(models[0].is_valid());
|
||||
}
|
||||
|
||||
TEST(FormatIOTest, IGESRoundTrip_AutoDetect) {
|
||||
auto box = make_box(2, 2, 2);
|
||||
std::string iges = export_iges({box});
|
||||
|
||||
auto fmt = detect_format_from_data(iges);
|
||||
EXPECT_EQ(fmt, CadFormat::IGES);
|
||||
|
||||
auto models = auto_import_from_string(iges);
|
||||
EXPECT_GE(models.size(), 0u);
|
||||
}
|
||||
Reference in New Issue
Block a user