feat(v4.1): incremental update + large assembly + format robustness
CI / Build & Test (push) Failing after 35s
CI / Release Build (push) Failing after 48s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

- 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:
茂之钳
2026-07-25 03:51:57 +00:00
parent 89c2525f9f
commit 2e004fda6d
11 changed files with 1223 additions and 0 deletions
+2
View File
@@ -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)
+109
View File
@@ -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);
}
+218
View File
@@ -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);
}