From b96b6defd682eb900855bbbeda77f6f2ad54c84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=82=E4=B9=8B=E9=92=B3?= Date: Fri, 24 Jul 2026 15:36:42 +0000 Subject: [PATCH] feat(v3.9): parallel OpenMP boolean + assembly instancing + update notifier --- CMakeLists.txt | 1 + include/vde/brep/assembly_instance.h | 56 +++++++++++++++++ include/vde/brep/update_notifier.h | 41 +++++++++++++ src/CMakeLists.txt | 13 ++++ src/brep/assembly_instance.cpp | 33 ++++++++++ src/brep/brep_boolean.cpp | 56 +++++++++++++++-- tests/brep/CMakeLists.txt | 1 + tests/brep/test_assembly_instance.cpp | 87 +++++++++++++++++++++++++++ 8 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 include/vde/brep/assembly_instance.h create mode 100644 include/vde/brep/update_notifier.h create mode 100644 src/brep/assembly_instance.cpp create mode 100644 tests/brep/test_assembly_instance.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 222a455..0e29d36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ option(BUILD_EXAMPLES "Build examples" ON) option(ENABLE_SANITIZERS "Enable ASan" OFF) option(VDE_USE_GMP "GMP exact arithmetic" OFF) option(VDE_BUILD_PYTHON "Python bindings" OFF) +option(VDE_USE_OPENMP "Enable OpenMP parallelization" ON) add_library(vde_compile_options INTERFACE) include(cmake/CompilerSettings.cmake) diff --git a/include/vde/brep/assembly_instance.h b/include/vde/brep/assembly_instance.h new file mode 100644 index 0000000..51cf2e7 --- /dev/null +++ b/include/vde/brep/assembly_instance.h @@ -0,0 +1,56 @@ +#pragma once +#include "vde/brep/assembly.h" +#include +#include +#include + +namespace vde::brep { + +/// Shared geometry instance (for repeated parts) +struct GeometryInstance { + std::string part_name; + BrepModel geometry; ///< shared geometry data + size_t use_count = 0; ///< number of references to this geometry +}; + +/** + * @brief Instance manager for large assemblies. + * + * Deduplicates repeated parts by reusing shared geometry. + * Reduces memory and improves cache coherence for assemblies + * with thousands of identical components (fasteners, links, panels). + * + * @ingroup brep + */ +class AssemblyInstancer { +public: + /** + * @brief Register a geometry for instancing. + * @param name Part name identifier + * @param geom Geometry to register + * @return instance ID, reuses existing if same name & geometry matches + */ + int register_instance(const std::string& name, const BrepModel& geom); + + /** + * @brief Create an occurrence of a registered instance. + * @param assy Target assembly + * @param parent Parent node (nullptr = root) + * @param instance_id Instance to place + * @param transform Local transform for this occurrence + */ + void place_instance(Assembly& assy, AssemblyNode* parent, + int instance_id, const Transform3D& transform); + + /// Total unique geometries + [[nodiscard]] size_t unique_count() const { return instances_.size(); } + + /// Total placed occurrences + [[nodiscard]] size_t total_placed() const { return total_placed_; } + +private: + std::vector instances_; + size_t total_placed_ = 0; +}; + +} // namespace vde::brep diff --git a/include/vde/brep/update_notifier.h b/include/vde/brep/update_notifier.h new file mode 100644 index 0000000..6d666e1 --- /dev/null +++ b/include/vde/brep/update_notifier.h @@ -0,0 +1,41 @@ +#pragma once +#include "vde/brep/feature_tree.h" +#include +#include +#include + +namespace vde::brep { + +/// Callback type for model changes. +/// Receives the feature name and the new model after rebuild. +using UpdateCallback = std::function; + +/** + * @brief Notifies listeners when features change. + * + * Integrates with FeatureHistory: after each apply/undo/redo, + * call notify() to push the updated model to all registered callbacks. + * + * @ingroup brep + */ +class UpdateNotifier { +public: + /// Register a callback. Called on every notify(). + void on_update(UpdateCallback cb) { callbacks_.push_back(std::move(cb)); } + + /// Notify all registered listeners. + void notify(const std::string& feature, const BrepModel& model) { + for (auto& cb : callbacks_) cb(feature, model); + } + + /// Remove all callbacks. + void clear() { callbacks_.clear(); } + + /// Number of registered callbacks. + [[nodiscard]] size_t listener_count() const { return callbacks_.size(); } + +private: + std::vector callbacks_; +}; + +} // namespace vde::brep diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 14c5ea5..5b10b40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -158,6 +158,7 @@ add_library(vde_brep STATIC brep/assembly_constraints.cpp brep/measure.cpp brep/trimmed_surface.cpp + brep/assembly_instance.cpp ) target_include_directories(vde_brep PUBLIC ${CMAKE_SOURCE_DIR}/include @@ -219,3 +220,15 @@ if(VDE_USE_GMP) else() message(STATUS "GMP disabled (-DVDE_USE_GMP=ON to enable)") endif() + +# ── OpenMP parallelization ── +if(VDE_USE_OPENMP) + find_package(OpenMP) + if(OpenMP_CXX_FOUND) + target_link_libraries(vde_brep PUBLIC OpenMP::OpenMP_CXX) + target_compile_definitions(vde_brep PUBLIC VDE_USE_OPENMP) + message(STATUS "OpenMP enabled") + else() + message(STATUS "OpenMP not found (parallelism disabled)") + endif() +endif() diff --git a/src/brep/assembly_instance.cpp b/src/brep/assembly_instance.cpp new file mode 100644 index 0000000..99813a2 --- /dev/null +++ b/src/brep/assembly_instance.cpp @@ -0,0 +1,33 @@ +#include "vde/brep/assembly_instance.h" + +namespace vde::brep { + +int AssemblyInstancer::register_instance(const std::string& name, const BrepModel& geom) { + // Check for existing match: same name + similar bounding box + for (size_t i = 0; i < instances_.size(); ++i) { + if (instances_[i].part_name == name) { + auto bb1 = instances_[i].geometry.bounds(); + auto bb2 = geom.bounds(); + if ((bb1.min() - bb2.min()).norm() < 1e-3) { + instances_[i].use_count++; + return static_cast(i); + } + } + } + instances_.push_back({name, geom, 1}); + return static_cast(instances_.size() - 1); +} + +void AssemblyInstancer::place_instance(Assembly& assy, AssemblyNode* parent, + int instance_id, const Transform3D& transform) { + if (instance_id < 0 || instance_id >= static_cast(instances_.size())) return; + auto& inst = instances_[instance_id]; + if (parent) { + parent->add_part(inst.part_name, inst.geometry, transform); + } else { + assy.root.add_part(inst.part_name, inst.geometry, transform); + } + total_placed_++; +} + +} // namespace vde::brep diff --git a/src/brep/brep_boolean.cpp b/src/brep/brep_boolean.cpp index 5e6f6df..cbca194 100644 --- a/src/brep/brep_boolean.cpp +++ b/src/brep/brep_boolean.cpp @@ -11,6 +11,11 @@ #include #include #include +#include + +#ifdef VDE_USE_OPENMP +#include +#endif namespace vde::brep { @@ -562,8 +567,12 @@ BrepModel brep_union(const BrepModel& a, const BrepModel& b) { // then classify fragments. Keep OUT and ON (from A). // For each face of B: same against A, keep only OUT. std::vector keep; + std::mutex keep_mutex; // A faces: SSI-split against B, classify, keep OUT/ON +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fia = 0; fia < a.num_faces(); ++fia) { std::vector fragments; fragments.push_back(extract_face_fragment(a, static_cast(fia))); @@ -589,11 +598,17 @@ BrepModel brep_union(const BrepModel& a, const BrepModel& b) { // Classify each fragment against B for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, b); - if (cls == OUT || cls == ON) keep.push_back(std::move(frag)); + if (cls == OUT || cls == ON) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } // B faces: SSI-split against A, classify, keep OUT only (ON from A already) +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fib = 0; fib < b.num_faces(); ++fib) { std::vector fragments; fragments.push_back(extract_face_fragment(b, static_cast(fib))); @@ -615,7 +630,10 @@ BrepModel brep_union(const BrepModel& a, const BrepModel& b) { for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, a); - if (cls == OUT) keep.push_back(std::move(frag)); + if (cls == OUT) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } @@ -634,8 +652,12 @@ BrepModel brep_intersection(const BrepModel& a, const BrepModel& b) { // For intersection: keep fragments IN the other body, // and fragments ON the boundary (only from A to avoid duplicates). std::vector keep; + std::mutex keep_mutex; // A faces: SSI-split against B, classify, keep IN/ON +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fia = 0; fia < a.num_faces(); ++fia) { std::vector fragments; fragments.push_back(extract_face_fragment(a, static_cast(fia))); @@ -657,11 +679,17 @@ BrepModel brep_intersection(const BrepModel& a, const BrepModel& b) { for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, b); - if (cls == IN || cls == ON) keep.push_back(std::move(frag)); + if (cls == IN || cls == ON) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } // B faces: SSI-split against A, classify, keep IN only +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fib = 0; fib < b.num_faces(); ++fib) { std::vector fragments; fragments.push_back(extract_face_fragment(b, static_cast(fib))); @@ -683,7 +711,10 @@ BrepModel brep_intersection(const BrepModel& a, const BrepModel& b) { for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, a); - if (cls == IN) keep.push_back(std::move(frag)); + if (cls == IN) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } @@ -704,8 +735,12 @@ BrepModel brep_difference(const BrepModel& a, const BrepModel& b) { // - Keep A fragments that are OUT of B (discard IN and ON) // - Keep B fragments that are IN A (forms the inner cut surface) std::vector keep; + std::mutex keep_mutex; // A faces: SSI-split against B, classify, keep OUT +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fia = 0; fia < a.num_faces(); ++fia) { std::vector fragments; fragments.push_back(extract_face_fragment(a, static_cast(fia))); @@ -727,11 +762,17 @@ BrepModel brep_difference(const BrepModel& a, const BrepModel& b) { for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, b); - if (cls == OUT) keep.push_back(std::move(frag)); + if (cls == OUT) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } // B faces: SSI-split against A, classify, keep IN (forms inner cut surface) +#ifdef VDE_USE_OPENMP +#pragma omp parallel for schedule(dynamic) +#endif for (size_t fib = 0; fib < b.num_faces(); ++fib) { std::vector fragments; fragments.push_back(extract_face_fragment(b, static_cast(fib))); @@ -753,7 +794,10 @@ BrepModel brep_difference(const BrepModel& a, const BrepModel& b) { for (auto& frag : fragments) { auto cls = classify_face_fragment(frag, a); - if (cls == IN) keep.push_back(std::move(frag)); + if (cls == IN) { + std::lock_guard lock(keep_mutex); + keep.push_back(std::move(frag)); + } } } diff --git a/tests/brep/CMakeLists.txt b/tests/brep/CMakeLists.txt index 3193c52..246edb2 100644 --- a/tests/brep/CMakeLists.txt +++ b/tests/brep/CMakeLists.txt @@ -13,3 +13,4 @@ add_vde_test(test_assembly_constraints) add_vde_test(test_trimmed_surface) add_vde_test(test_brep_heal) add_vde_test(test_brep_drawing) +add_vde_test(test_assembly_instance) diff --git a/tests/brep/test_assembly_instance.cpp b/tests/brep/test_assembly_instance.cpp new file mode 100644 index 0000000..3d93d09 --- /dev/null +++ b/tests/brep/test_assembly_instance.cpp @@ -0,0 +1,87 @@ +#include +#include "vde/brep/assembly_instance.h" +#include "vde/brep/modeling.h" + +using namespace vde::brep; + +TEST(AssemblyInstancer, IdenticalBoxesReturnSameID) { + AssemblyInstancer instancer; + + auto box1 = make_box(10, 5, 3); + auto box2 = make_box(10, 5, 3); + + int id1 = instancer.register_instance("bolt", box1); + int id2 = instancer.register_instance("bolt", box2); + + EXPECT_EQ(id1, id2); + EXPECT_EQ(instancer.unique_count(), 1); +} + +TEST(AssemblyInstancer, DifferentBoxesReturnDifferentIDs) { + AssemblyInstancer instancer; + + auto box1 = make_box(10, 5, 3); + auto box2 = make_box(5, 5, 5); + + int id1 = instancer.register_instance("bolt", box1); + int id2 = instancer.register_instance("bolt", box2); + + EXPECT_NE(id1, id2); + EXPECT_EQ(instancer.unique_count(), 2); +} + +TEST(AssemblyInstancer, DifferentNamesYieldDifferentIDs) { + AssemblyInstancer instancer; + + auto box = make_box(10, 5, 3); + + int id1 = instancer.register_instance("bolt", box); + int id2 = instancer.register_instance("screw", box); + + EXPECT_NE(id1, id2); + EXPECT_EQ(instancer.unique_count(), 2); +} + +TEST(AssemblyInstancer, PlaceInstancesCountsCorrectly) { + AssemblyInstancer instancer; + auto box = make_box(2, 2, 2); + int id = instancer.register_instance("cell", box); + + Assembly assy("grid"); + + for (int i = 0; i < 100; ++i) { + auto xform = Transform3D::Translation(i % 10, i / 10, 0); + instancer.place_instance(assy, nullptr, id, xform); + } + + EXPECT_EQ(instancer.total_placed(), 100); + EXPECT_EQ(instancer.unique_count(), 1); + EXPECT_EQ(assy.part_count(), 100); +} + +TEST(AssemblyInstancer, PlaceInstanceWithParent) { + AssemblyInstancer instancer; + auto box = make_box(1, 1, 1); + int id = instancer.register_instance("leaf", box); + + Assembly assy("tree"); + + // Place a child under a subassembly + auto* sub = assy.root.add_subassembly("sub", Transform3D::Identity()); + auto xform = Transform3D::Translation(5, 0, 0); + EXPECT_NO_THROW(instancer.place_instance(assy, sub, id, xform)); + + EXPECT_EQ(instancer.total_placed(), 1); + EXPECT_EQ(assy.root.children.size(), 1); + EXPECT_EQ(assy.root.children[0]->name, "sub"); + EXPECT_EQ(assy.root.children[0]->children.size(), 1); +} + +TEST(AssemblyInstancer, InvalidInstanceIdNoOp) { + AssemblyInstancer instancer; + Assembly assy("empty"); + + EXPECT_NO_THROW(instancer.place_instance(assy, nullptr, -1, Transform3D::Identity())); + EXPECT_NO_THROW(instancer.place_instance(assy, nullptr, 999, Transform3D::Identity())); + EXPECT_EQ(instancer.total_placed(), 0); +}