From: Pasukhin Dmitry Date: Wed, 26 Nov 2025 10:38:42 +0000 (+0000) Subject: Foundation Classes - Enhance BVH Implementation (#842) X-Git-Url: http://git.dev.opencascade.org/gitweb/?a=commitdiff_plain;h=8a37fbd49f5e6d9a2444def0879fd852b36e03de;p=occt.git Foundation Classes - Enhance BVH Implementation (#842) - Fixed leaf node size condition and SAH cost evaluation in `BVH_SweepPlaneBuilder` - Added `constexpr` to `BVH_Box`, `BVH_Types`, and helper functions for compile-time evaluation - Introduced 13 new comprehensive test files covering BVH components - Removed unused `BVH_BuildQueue.cxx` file - Added internal helper structures to `BVH_Traverse` classes --- diff --git a/src/FoundationClasses/TKMath/BVH/BVH_BinnedBuilder.hxx b/src/FoundationClasses/TKMath/BVH/BVH_BinnedBuilder.hxx index ca5dde4134..a648316874 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_BinnedBuilder.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_BinnedBuilder.hxx @@ -218,7 +218,8 @@ typename BVH_QueueBuilder::BVH_ChildNodes BVH_BinnedBuilder::b { const Standard_Integer aNodeBegPrimitive = theBVH->BegPrimitive(theNode); const Standard_Integer aNodeEndPrimitive = theBVH->EndPrimitive(theNode); - if (aNodeEndPrimitive - aNodeBegPrimitive < BVH_Builder::myLeafNodeSize) + const Standard_Integer aNodeNbPrimitives = theBVH->NbPrimitives(theNode); + if (aNodeNbPrimitives <= BVH_Builder::myLeafNodeSize) { // clang-format off return typename BVH_QueueBuilder::BVH_ChildNodes(); // node does not require partitioning @@ -270,14 +271,16 @@ typename BVH_QueueBuilder::BVH_ChildNodes BVH_BinnedBuilder::b } // Choose the best split (with minimum SAH cost) + const Standard_Real aParentArea = static_cast(anAABB.Area()); for (Standard_Integer aSplit = 1; aSplit < Bins; ++aSplit) { - // Simple SAH evaluation - Standard_Real aCost = - (static_cast(aSplitPlanes[aSplit].LftVoxel.Box.Area()) /* / S(N) */) - * aSplitPlanes[aSplit].LftVoxel.Count - + (static_cast(aSplitPlanes[aSplit].RghVoxel.Box.Area()) /* / S(N) */) - * aSplitPlanes[aSplit].RghVoxel.Count; + // SAH evaluation with proper normalization by parent surface area + const Standard_Real aLftArea = + static_cast(aSplitPlanes[aSplit].LftVoxel.Box.Area()); + const Standard_Real aRghArea = + static_cast(aSplitPlanes[aSplit].RghVoxel.Box.Area()); + Standard_Real aCost = (aLftArea / aParentArea) * aSplitPlanes[aSplit].LftVoxel.Count + + (aRghArea / aParentArea) * aSplitPlanes[aSplit].RghVoxel.Count; if (aCost <= aMinSplitCost) { diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Box.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Box.hxx index cbf6ca1722..d511df0f17 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Box.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Box.hxx @@ -125,13 +125,13 @@ public: public: //! Creates uninitialized bounding box. - BVH_Box() + constexpr BVH_Box() noexcept : myIsInited(Standard_False) { } //! Creates bounding box of given point. - BVH_Box(const BVH_VecNt& thePoint) + constexpr BVH_Box(const BVH_VecNt& thePoint) noexcept : myMinPoint(thePoint), myMaxPoint(thePoint), myIsInited(Standard_True) @@ -139,7 +139,7 @@ public: } //! Creates bounding box from corner points. - BVH_Box(const BVH_VecNt& theMinPoint, const BVH_VecNt& theMaxPoint) + constexpr BVH_Box(const BVH_VecNt& theMinPoint, const BVH_VecNt& theMaxPoint) noexcept : myMinPoint(theMinPoint), myMaxPoint(theMaxPoint), myIsInited(Standard_True) @@ -148,10 +148,10 @@ public: public: //! Clears bounding box. - void Clear() { myIsInited = Standard_False; } + constexpr void Clear() noexcept { myIsInited = Standard_False; } //! Is bounding box valid? - Standard_Boolean IsValid() const { return myIsInited; } + constexpr Standard_Boolean IsValid() const noexcept { return myIsInited; } //! Appends new point to the bounding box. void Add(const BVH_VecNt& thePoint) @@ -173,29 +173,29 @@ public: void Combine(const BVH_Box& theBox); //! Returns minimum point of bounding box. - const BVH_VecNt& CornerMin() const { return myMinPoint; } + constexpr const BVH_VecNt& CornerMin() const noexcept { return myMinPoint; } //! Returns maximum point of bounding box. - const BVH_VecNt& CornerMax() const { return myMaxPoint; } + constexpr const BVH_VecNt& CornerMax() const noexcept { return myMaxPoint; } //! Returns minimum point of bounding box. - BVH_VecNt& CornerMin() { return myMinPoint; } + constexpr BVH_VecNt& CornerMin() noexcept { return myMinPoint; } //! Returns maximum point of bounding box. - BVH_VecNt& CornerMax() { return myMaxPoint; } + constexpr BVH_VecNt& CornerMax() noexcept { return myMaxPoint; } //! Returns surface area of bounding box. //! If the box is degenerated into line, returns the perimeter instead. T Area() const; //! Returns diagonal of bounding box. - BVH_VecNt Size() const { return myMaxPoint - myMinPoint; } + constexpr BVH_VecNt Size() const { return myMaxPoint - myMinPoint; } //! Returns center of bounding box. - BVH_VecNt Center() const { return (myMinPoint + myMaxPoint) * static_cast(0.5); } + constexpr BVH_VecNt Center() const { return (myMinPoint + myMaxPoint) * static_cast(0.5); } //! Returns center of bounding box along the given axis. - T Center(const Standard_Integer theAxis) const; + inline T Center(const Standard_Integer theAxis) const; //! Dumps the content of me into the stream void DumpJson(Standard_OStream& theOStream, Standard_Integer theDepth = -1) const @@ -203,18 +203,18 @@ public: (void)theDepth; OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myIsInited) - int n = (std::min)(N, 3); - if (n == 1) + constexpr int n = (N < 3) ? N : 3; + if constexpr (n == 1) { OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myMinPoint[0]) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myMinPoint[0]) } - else if (n == 2) + else if constexpr (n == 2) { OCCT_DUMP_FIELD_VALUES_NUMERICAL(theOStream, "MinPoint", n, myMinPoint[0], myMinPoint[1]) OCCT_DUMP_FIELD_VALUES_NUMERICAL(theOStream, "MaxPoint", n, myMaxPoint[0], myMaxPoint[1]) } - else if (n == 3) + else if constexpr (n == 3) { OCCT_DUMP_FIELD_VALUES_NUMERICAL(theOStream, "MinPoint", @@ -242,14 +242,14 @@ public: OCCT_INIT_FIELD_VALUE_INTEGER(aStreamStr, aPos, anIsInited); myIsInited = anIsInited != 0; - int n = (std::min)(N, 3); - if (n == 1) + constexpr int n = (N < 3) ? N : 3; + if constexpr (n == 1) { Standard_Real aValue; OCCT_INIT_FIELD_VALUE_REAL(aStreamStr, aPos, aValue); myMinPoint[0] = (T)aValue; } - else if (n == 2) + else if constexpr (n == 2) { Standard_Real aValue1, aValue2; OCCT_INIT_VECTOR_CLASS(aStreamStr, "MinPoint", aPos, n, &aValue1, &aValue2); @@ -260,7 +260,7 @@ public: myMaxPoint[0] = (T)aValue1; myMaxPoint[1] = (T)aValue2; } - else if (n == 3) + else if constexpr (n == 3) { Standard_Real aValue1, aValue2, aValue3; OCCT_INIT_VECTOR_CLASS(aStreamStr, "MinPoint", aPos, n, &aValue1, &aValue2, &aValue3); @@ -280,7 +280,7 @@ public: public: //! Checks if the Box is out of the other box. - Standard_Boolean IsOut(const BVH_Box& theOther) const + constexpr Standard_Boolean IsOut(const BVH_Box& theOther) const { if (!theOther.IsValid()) return Standard_True; @@ -289,22 +289,32 @@ public: } //! Checks if the Box is out of the other box defined by two points. - Standard_Boolean IsOut(const BVH_VecNt& theMinPoint, const BVH_VecNt& theMaxPoint) const + constexpr Standard_Boolean IsOut(const BVH_VecNt& theMinPoint, const BVH_VecNt& theMaxPoint) const { if (!IsValid()) return Standard_True; - int n = (std::min)(N, 3); - for (int i = 0; i < n; ++i) + if constexpr (N >= 1) { - if (myMinPoint[i] > theMaxPoint[i] || myMaxPoint[i] < theMinPoint[i]) + if (myMinPoint[0] > theMaxPoint[0] || myMaxPoint[0] < theMinPoint[0]) + return Standard_True; + } + if constexpr (N >= 2) + { + if (myMinPoint[1] > theMaxPoint[1] || myMaxPoint[1] < theMinPoint[1]) + return Standard_True; + } + if constexpr (N >= 3) + { + if (myMinPoint[2] > theMaxPoint[2] || myMaxPoint[2] < theMinPoint[2]) return Standard_True; } return Standard_False; } //! Checks if the Box fully contains the other box. - Standard_Boolean Contains(const BVH_Box& theOther, Standard_Boolean& hasOverlap) const + constexpr Standard_Boolean Contains(const BVH_Box& theOther, + Standard_Boolean& hasOverlap) const { hasOverlap = Standard_False; if (!theOther.IsValid()) @@ -314,9 +324,9 @@ public: } //! Checks if the Box is fully contains the other box. - Standard_Boolean Contains(const BVH_VecNt& theMinPoint, - const BVH_VecNt& theMaxPoint, - Standard_Boolean& hasOverlap) const + constexpr Standard_Boolean Contains(const BVH_VecNt& theMinPoint, + const BVH_VecNt& theMaxPoint, + Standard_Boolean& hasOverlap) const { hasOverlap = Standard_False; if (!IsValid()) @@ -324,28 +334,49 @@ public: Standard_Boolean isInside = Standard_True; - int n = (std::min)(N, 3); - for (int i = 0; i < n; ++i) + if constexpr (N >= 1) { - hasOverlap = (myMinPoint[i] <= theMaxPoint[i] && myMaxPoint[i] >= theMinPoint[i]); + hasOverlap = (myMinPoint[0] <= theMaxPoint[0] && myMaxPoint[0] >= theMinPoint[0]); if (!hasOverlap) return Standard_False; - - isInside = isInside && (myMinPoint[i] <= theMinPoint[i] && myMaxPoint[i] >= theMaxPoint[i]); + isInside = isInside && (myMinPoint[0] <= theMinPoint[0] && myMaxPoint[0] >= theMaxPoint[0]); + } + if constexpr (N >= 2) + { + hasOverlap = (myMinPoint[1] <= theMaxPoint[1] && myMaxPoint[1] >= theMinPoint[1]); + if (!hasOverlap) + return Standard_False; + isInside = isInside && (myMinPoint[1] <= theMinPoint[1] && myMaxPoint[1] >= theMaxPoint[1]); + } + if constexpr (N >= 3) + { + hasOverlap = (myMinPoint[2] <= theMaxPoint[2] && myMaxPoint[2] >= theMinPoint[2]); + if (!hasOverlap) + return Standard_False; + isInside = isInside && (myMinPoint[2] <= theMinPoint[2] && myMaxPoint[2] >= theMaxPoint[2]); } return isInside; } //! Checks if the Point is out of the box. - Standard_Boolean IsOut(const BVH_VecNt& thePoint) const + constexpr Standard_Boolean IsOut(const BVH_VecNt& thePoint) const { if (!IsValid()) return Standard_True; - int n = (std::min)(N, 3); - for (int i = 0; i < n; ++i) + if constexpr (N >= 1) + { + if (thePoint[0] < myMinPoint[0] || thePoint[0] > myMaxPoint[0]) + return Standard_True; + } + if constexpr (N >= 2) + { + if (thePoint[1] < myMinPoint[1] || thePoint[1] > myMaxPoint[1]) + return Standard_True; + } + if constexpr (N >= 3) { - if (thePoint[i] < myMinPoint[i] || thePoint[i] > myMaxPoint[i]) + if (thePoint[2] < myMinPoint[2] || thePoint[2] > myMaxPoint[2]) return Standard_True; } return Standard_False; @@ -371,7 +402,7 @@ struct CenterAxis template struct CenterAxis { - static T Center(const BVH_Box& theBox, const Standard_Integer theAxis) + static inline T Center(const BVH_Box& theBox, const Standard_Integer theAxis) { if (theAxis == 0) { @@ -388,7 +419,7 @@ struct CenterAxis template struct CenterAxis { - static T Center(const BVH_Box& theBox, const Standard_Integer theAxis) + static inline T Center(const BVH_Box& theBox, const Standard_Integer theAxis) { if (theAxis == 0) { @@ -409,7 +440,7 @@ struct CenterAxis template struct CenterAxis { - static T Center(const BVH_Box& theBox, const Standard_Integer theAxis) + static inline T Center(const BVH_Box& theBox, const Standard_Integer theAxis) { if (theAxis == 0) { @@ -439,7 +470,7 @@ struct SurfaceCalculator template struct SurfaceCalculator { - static T Area(const typename BVH_Box::BVH_VecNt& theSize) + static inline T Area(const typename BVH_Box::BVH_VecNt& theSize) { const T anArea = std::abs(theSize.x() * theSize.y()); @@ -455,7 +486,7 @@ struct SurfaceCalculator template struct SurfaceCalculator { - static T Area(const typename BVH_Box::BVH_VecNt& theSize) + static inline T Area(const typename BVH_Box::BVH_VecNt& theSize) { const T anArea = (std::abs(theSize.x() * theSize.y()) + std::abs(theSize.x() * theSize.z()) + std::abs(theSize.z() * theSize.y())) @@ -473,7 +504,7 @@ struct SurfaceCalculator template struct SurfaceCalculator { - static T Area(const typename BVH_Box::BVH_VecNt& theSize) + static inline T Area(const typename BVH_Box::BVH_VecNt& theSize) { const T anArea = (std::abs(theSize.x() * theSize.y()) + std::abs(theSize.x() * theSize.z()) + std::abs(theSize.z() * theSize.y())) @@ -497,14 +528,14 @@ struct BoxMinMax { typedef typename BVH::VectorType::Type BVH_VecNt; - static void CwiseMin(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) + static inline void CwiseMin(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) { theVec1.x() = (std::min)(theVec1.x(), theVec2.x()); theVec1.y() = (std::min)(theVec1.y(), theVec2.y()); theVec1.z() = (std::min)(theVec1.z(), theVec2.z()); } - static void CwiseMax(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) + static inline void CwiseMax(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) { theVec1.x() = (std::max)(theVec1.x(), theVec2.x()); theVec1.y() = (std::max)(theVec1.y(), theVec2.y()); @@ -517,13 +548,13 @@ struct BoxMinMax { typedef typename BVH::VectorType::Type BVH_VecNt; - static void CwiseMin(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) + static inline void CwiseMin(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) { theVec1.x() = (std::min)(theVec1.x(), theVec2.x()); theVec1.y() = (std::min)(theVec1.y(), theVec2.y()); } - static void CwiseMax(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) + static inline void CwiseMax(BVH_VecNt& theVec1, const BVH_VecNt& theVec2) { theVec1.x() = (std::max)(theVec1.x(), theVec2.x()); theVec1.y() = (std::max)(theVec1.y(), theVec2.y()); diff --git a/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.cxx b/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.cxx deleted file mode 100644 index c9850242b1..0000000000 --- a/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.cxx +++ /dev/null @@ -1,69 +0,0 @@ -// Created on: 2015-05-27 -// Created by: Denis BOGOLEPOV -// Copyright (c) 2015 OPEN CASCADE SAS -// -// This file is part of Open CASCADE Technology software library. -// -// This library is free software; you can redistribute it and/or modify it under -// the terms of the GNU Lesser General Public License version 2.1 as published -// by the Free Software Foundation, with special exception defined in the file -// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT -// distribution for complete text of the license and disclaimer of any warranty. -// -// Alternatively, this file may be used under the terms of Open CASCADE -// commercial license or contractual agreement. - -#include - -// ======================================================================= -// function : Size -// purpose : Returns current size of BVH build queue -// ======================================================================= -Standard_Integer BVH_BuildQueue::Size() -{ - std::lock_guard aLock(myMutex); - return myQueue.Size(); -} - -// ======================================================================= -// function : Enqueue -// purpose : Enqueues new work-item onto BVH build queue -// ======================================================================= -void BVH_BuildQueue::Enqueue(const Standard_Integer& theWorkItem) -{ - std::lock_guard aLock(myMutex); - myQueue.Append(theWorkItem); -} - -// ======================================================================= -// function : Fetch -// purpose : Fetches first work-item from BVH build queue -// ======================================================================= -Standard_Integer BVH_BuildQueue::Fetch(Standard_Boolean& wasBusy) -{ - std::lock_guard aLock(myMutex); - - Standard_Integer aQuery = -1; - if (!myQueue.IsEmpty()) - { - aQuery = myQueue.First(); - - myQueue.Remove(1); // remove item from queue - } - - if (aQuery != -1) - { - if (!wasBusy) - { - ++myNbThreads; - } - } - else if (wasBusy) - { - --myNbThreads; - } - - wasBusy = aQuery != -1; - - return aQuery; -} diff --git a/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.hxx b/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.hxx index b83762030b..4afa167108 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_BuildQueue.hxx @@ -20,6 +20,7 @@ #include +#include #include //! Command-queue for parallel building of BVH nodes. @@ -31,40 +32,82 @@ class BVH_BuildQueue public: //! Creates new BVH build queue. BVH_BuildQueue() - : myNbThreads(0) + : myNbThreads(0), + mySize(0) { - // } //! Releases resources of BVH build queue. - ~BVH_BuildQueue() - { - // - } + ~BVH_BuildQueue() = default; public: //! Returns current size of BVH build queue. - Standard_EXPORT Standard_Integer Size(); + //! Uses acquire semantics to synchronize with enqueue/dequeue operations. + Standard_Integer Size() const { return mySize.load(std::memory_order_acquire); } //! Enqueues new work-item onto BVH build queue. - Standard_EXPORT void Enqueue(const Standard_Integer& theNode); + void Enqueue(const Standard_Integer theWorkItem) + { + std::lock_guard aLock(myMutex); + myQueue.Append(theWorkItem); + mySize.fetch_add(1, std::memory_order_release); + } //! Fetches first work-item from BVH build queue. - Standard_EXPORT Standard_Integer Fetch(Standard_Boolean& wasBusy); + Standard_Integer Fetch(Standard_Boolean& wasBusy) + { + Standard_Integer aQuery = -1; + + // Fetch item from queue under lock + { + std::lock_guard aLock(myMutex); + if (!myQueue.IsEmpty()) + { + aQuery = myQueue.First(); + myQueue.Remove(1); + mySize.fetch_sub(1, std::memory_order_release); + } + } + + // Update thread counter atomically with release/acquire semantics + // to ensure proper synchronization with HasBusyThreads() + if (aQuery != -1) + { + if (!wasBusy) + { + myNbThreads.fetch_add(1, std::memory_order_release); + } + } + else if (wasBusy) + { + myNbThreads.fetch_sub(1, std::memory_order_release); + } + + wasBusy = (aQuery != -1); + return aQuery; + } //! Checks if there are active build threads. - Standard_Boolean HasBusyThreads() { return myNbThreads != 0; } + //! Uses acquire semantics to ensure visibility of thread counter updates. + //! This is critical for termination detection: threads check this after + //! finding an empty queue to determine if they should exit or wait. + Standard_Boolean HasBusyThreads() const + { + return myNbThreads.load(std::memory_order_acquire) != 0; + } -protected: +private: //! Queue of BVH nodes to build. NCollection_Sequence myQueue; -protected: - //! Manages access serialization of working threads. + //! Manages access serialization for queue operations. std::mutex myMutex; - //! Number of active build threads. - Standard_Integer myNbThreads; + //! Number of active build threads (atomic for lock-free reads). + std::atomic myNbThreads; + + //! Current queue size (atomic for lock-free reads). + std::atomic mySize; }; #endif // _BVH_BuildQueue_Header diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Constants.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Constants.hxx index ca440ecd69..cea52735c3 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Constants.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Constants.hxx @@ -42,7 +42,7 @@ enum namespace BVH { //! Minimum node size to split. -const double THE_NODE_MIN_SIZE = 1e-5; +constexpr double THE_NODE_MIN_SIZE = 1e-5; } // namespace BVH #endif // _BVH_Constants_Header diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Geometry.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Geometry.hxx index 36e8826021..8c289a0c38 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Geometry.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Geometry.hxx @@ -107,8 +107,8 @@ protected: protected: Standard_Boolean myIsDirty; //!< Is geometry state outdated? - opencascade::handle> myBVH; //!< Constructed hight-level BVH - opencascade::handle> myBuilder; //!< Builder for hight-level BVH + opencascade::handle> myBVH; //!< Constructed high-level BVH + opencascade::handle> myBuilder; //!< Builder for high-level BVH mutable BVH_Box myBox; //!< Cached bounding box of geometric objects }; diff --git a/src/FoundationClasses/TKMath/BVH/BVH_LinearBuilder.hxx b/src/FoundationClasses/TKMath/BVH/BVH_LinearBuilder.hxx index 1777830c67..192294ab9a 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_LinearBuilder.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_LinearBuilder.hxx @@ -17,6 +17,7 @@ #define _BVH_LinearBuilder_Header #include +#include #include //! Performs fast BVH construction using LBVH building approach. @@ -246,8 +247,7 @@ public: const Standard_Integer aLftChild = theData.myBVH->NodeInfoBuffer()[theData.myNode].y(); const Standard_Integer aRghChild = theData.myBVH->NodeInfoBuffer()[theData.myNode].z(); - std::vector> aList; - aList.reserve(2); + NCollection_Vector> aList(2); if (!theData.myBVH->IsOuter(aLftChild)) { BoundData aBoundData = {theData.mySet, @@ -255,7 +255,7 @@ public: aLftChild, theData.myLevel + 1, &aLftHeight}; - aList.push_back(aBoundData); + aList.Append(aBoundData); } else { @@ -269,14 +269,14 @@ public: aRghChild, theData.myLevel + 1, &aRghHeight}; - aList.push_back(aBoundData); + aList.Append(aBoundData); } else { aRghHeight = BVH::UpdateBounds(theData.mySet, theData.myBVH, aRghChild); } - if (!aList.empty()) + if (aList.Size() > 0) { OSD_Parallel::ForEach(aList.begin(), aList.end(), diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Properties.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Properties.hxx index cba39c5540..25f9565877 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Properties.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Properties.hxx @@ -48,7 +48,7 @@ public: } //! Releases resources of transformation properties. - virtual ~BVH_Transform() {} + virtual ~BVH_Transform() = default; //! Returns transformation matrix. const BVH_MatNt& Transform() const { return myTransform; } diff --git a/src/FoundationClasses/TKMath/BVH/BVH_QueueBuilder.hxx b/src/FoundationClasses/TKMath/BVH/BVH_QueueBuilder.hxx index dbd3cea4df..5b71018970 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_QueueBuilder.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_QueueBuilder.hxx @@ -46,7 +46,7 @@ public: } //! Releases resources of BVH queue based builder. - virtual ~BVH_QueueBuilder() {} + virtual ~BVH_QueueBuilder() = default; public: //! Builds BVH using specific algorithm. diff --git a/src/FoundationClasses/TKMath/BVH/BVH_QuickSorter.hxx b/src/FoundationClasses/TKMath/BVH/BVH_QuickSorter.hxx index bdf428db1b..4311089bfe 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_QuickSorter.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_QuickSorter.hxx @@ -17,9 +17,14 @@ #define _BVH_QuickSorter_Header #include +#include + +#include +#include //! Performs centroid-based sorting of abstract set along -//! the given axis (X - 0, Y - 1, Z - 2) using quick sort. +//! the given axis (X - 0, Y - 1, Z - 2) using std::sort. +//! Uses introsort algorithm which guarantees O(n log n) complexity. template class BVH_QuickSorter : public BVH_Sorter { @@ -41,41 +46,44 @@ public: const Standard_Integer theStart, const Standard_Integer theFinal) Standard_OVERRIDE { - Standard_Integer aLft = theStart; - Standard_Integer aRgh = theFinal; - - T aPivot = theSet->Center((aRgh + aLft) / 2, myAxis); - while (aLft < aRgh) + const Standard_Integer aSize = theFinal - theStart + 1; + if (aSize <= 1) { - while (theSet->Center(aLft, myAxis) < aPivot && aLft < theFinal) - { - ++aLft; - } - - while (theSet->Center(aRgh, myAxis) > aPivot && aRgh > theStart) - { - --aRgh; - } + return; + } - if (aLft <= aRgh) - { - if (aLft != aRgh) - { - theSet->Swap(aLft, aRgh); - } - ++aLft; - --aRgh; - } + // Create index array for sorting with OCCT allocator + std::vector> anIndices(aSize); + for (Standard_Integer i = 0; i < aSize; ++i) + { + anIndices[i] = i; } - if (aRgh > theStart) + // Sort indices by center value using std::sort (introsort - O(n log n) guaranteed) + const Standard_Integer anAxis = myAxis; + std::sort(anIndices.begin(), + anIndices.end(), + [theSet, theStart, anAxis](Standard_Integer a, Standard_Integer b) { + return theSet->Center(theStart + a, anAxis) < theSet->Center(theStart + b, anAxis); + }); + + // Compute inverse permutation: invPerm[i] = where element i should go + std::vector> anInvPerm(aSize); + for (Standard_Integer i = 0; i < aSize; ++i) { - Perform(theSet, theStart, aRgh); + anInvPerm[anIndices[i]] = i; } - if (aLft < theFinal) + // Apply permutation using cycle-based algorithm - O(n) swaps total + for (Standard_Integer i = 0; i < aSize; ++i) { - Perform(theSet, aLft, theFinal); + // Follow the cycle starting at position i + while (anInvPerm[i] != i) + { + Standard_Integer j = anInvPerm[i]; + theSet->Swap(theStart + i, theStart + j); + std::swap(anInvPerm[i], anInvPerm[j]); + } } } diff --git a/src/FoundationClasses/TKMath/BVH/BVH_RadixSorter.hxx b/src/FoundationClasses/TKMath/BVH/BVH_RadixSorter.hxx index 6d4f63572f..829dab3e66 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_RadixSorter.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_RadixSorter.hxx @@ -27,6 +27,60 @@ //! Pair of Morton code and primitive ID. typedef std::pair BVH_EncodedLink; +namespace BVH +{ +//! Lookup table for expanding 8-bit value to 24-bit Morton code component. +//! Each bit is spread to every 3rd position for interleaving with other components. +constexpr unsigned int THE_MORTON_LUT[256] = { + 0x000000, 0x000001, 0x000008, 0x000009, 0x000040, 0x000041, 0x000048, 0x000049, 0x000200, + 0x000201, 0x000208, 0x000209, 0x000240, 0x000241, 0x000248, 0x000249, 0x001000, 0x001001, + 0x001008, 0x001009, 0x001040, 0x001041, 0x001048, 0x001049, 0x001200, 0x001201, 0x001208, + 0x001209, 0x001240, 0x001241, 0x001248, 0x001249, 0x008000, 0x008001, 0x008008, 0x008009, + 0x008040, 0x008041, 0x008048, 0x008049, 0x008200, 0x008201, 0x008208, 0x008209, 0x008240, + 0x008241, 0x008248, 0x008249, 0x009000, 0x009001, 0x009008, 0x009009, 0x009040, 0x009041, + 0x009048, 0x009049, 0x009200, 0x009201, 0x009208, 0x009209, 0x009240, 0x009241, 0x009248, + 0x009249, 0x040000, 0x040001, 0x040008, 0x040009, 0x040040, 0x040041, 0x040048, 0x040049, + 0x040200, 0x040201, 0x040208, 0x040209, 0x040240, 0x040241, 0x040248, 0x040249, 0x041000, + 0x041001, 0x041008, 0x041009, 0x041040, 0x041041, 0x041048, 0x041049, 0x041200, 0x041201, + 0x041208, 0x041209, 0x041240, 0x041241, 0x041248, 0x041249, 0x048000, 0x048001, 0x048008, + 0x048009, 0x048040, 0x048041, 0x048048, 0x048049, 0x048200, 0x048201, 0x048208, 0x048209, + 0x048240, 0x048241, 0x048248, 0x048249, 0x049000, 0x049001, 0x049008, 0x049009, 0x049040, + 0x049041, 0x049048, 0x049049, 0x049200, 0x049201, 0x049208, 0x049209, 0x049240, 0x049241, + 0x049248, 0x049249, 0x200000, 0x200001, 0x200008, 0x200009, 0x200040, 0x200041, 0x200048, + 0x200049, 0x200200, 0x200201, 0x200208, 0x200209, 0x200240, 0x200241, 0x200248, 0x200249, + 0x201000, 0x201001, 0x201008, 0x201009, 0x201040, 0x201041, 0x201048, 0x201049, 0x201200, + 0x201201, 0x201208, 0x201209, 0x201240, 0x201241, 0x201248, 0x201249, 0x208000, 0x208001, + 0x208008, 0x208009, 0x208040, 0x208041, 0x208048, 0x208049, 0x208200, 0x208201, 0x208208, + 0x208209, 0x208240, 0x208241, 0x208248, 0x208249, 0x209000, 0x209001, 0x209008, 0x209009, + 0x209040, 0x209041, 0x209048, 0x209049, 0x209200, 0x209201, 0x209208, 0x209209, 0x209240, + 0x209241, 0x209248, 0x209249, 0x240000, 0x240001, 0x240008, 0x240009, 0x240040, 0x240041, + 0x240048, 0x240049, 0x240200, 0x240201, 0x240208, 0x240209, 0x240240, 0x240241, 0x240248, + 0x240249, 0x241000, 0x241001, 0x241008, 0x241009, 0x241040, 0x241041, 0x241048, 0x241049, + 0x241200, 0x241201, 0x241208, 0x241209, 0x241240, 0x241241, 0x241248, 0x241249, 0x248000, + 0x248001, 0x248008, 0x248009, 0x248040, 0x248041, 0x248048, 0x248049, 0x248200, 0x248201, + 0x248208, 0x248209, 0x248240, 0x248241, 0x248248, 0x248249, 0x249000, 0x249001, 0x249008, + 0x249009, 0x249040, 0x249041, 0x249048, 0x249049, 0x249200, 0x249201, 0x249208, 0x249209, + 0x249240, 0x249241, 0x249248, 0x249249}; + +//! Encodes 10-bit voxel coordinates into 30-bit Morton code using LUT. +//! @param theVoxelX X coordinate (0-1023) +//! @param theVoxelY Y coordinate (0-1023) +//! @param theVoxelZ Z coordinate (0-1023) +//! @return 30-bit Morton code with interleaved bits +constexpr unsigned int EncodeMortonCode(unsigned int theVoxelX, + unsigned int theVoxelY, + unsigned int theVoxelZ) +{ + // Split each 10-bit coordinate into two 8-bit lookups (upper 2 bits + lower 8 bits) + // For 10-bit values, we use lower 8 bits via LUT and handle upper 2 bits separately + return (THE_MORTON_LUT[theVoxelX & 0xFF] | (THE_MORTON_LUT[(theVoxelX >> 8) & 0x03] << 24)) + | ((THE_MORTON_LUT[theVoxelY & 0xFF] | (THE_MORTON_LUT[(theVoxelY >> 8) & 0x03] << 24)) + << 1) + | ((THE_MORTON_LUT[theVoxelZ & 0xFF] | (THE_MORTON_LUT[(theVoxelZ >> 8) & 0x03] << 24)) + << 2); +} +} // namespace BVH + //! Performs radix sort of a BVH primitive set using //! 10-bit Morton codes (or 1024 x 1024 x 1024 grid). template @@ -200,27 +254,26 @@ void BVH_RadixSorter::Perform(BVH_Set* theSet, myEncodedLinks = new NCollection_Shared>(theStart, theFinal); - // Step 1 -- Assign Morton code to each primitive + // Step 1 -- Assign Morton code to each primitive using LUT for faster encoding for (Standard_Integer aPrimIdx = theStart; aPrimIdx <= theFinal; ++aPrimIdx) { const BVH_VecNt aCenter = theSet->Box(aPrimIdx).Center(); const BVH_VecNt aVoxelF = (aCenter - aSceneMin) * aReverseSize; - unsigned int aMortonCode = 0; - for (Standard_Integer aCompIter = 0; aCompIter < aNbEffComp; ++aCompIter) - { - const Standard_Integer aVoxelI = BVH::IntFloor(BVH::VecComp::Get(aVoxelF, aCompIter)); - - unsigned int aVoxel = - static_cast((std::max)(0, (std::min)(aVoxelI, aDimension - 1))); - - aVoxel = (aVoxel | (aVoxel << 16)) & 0x030000FF; - aVoxel = (aVoxel | (aVoxel << 8)) & 0x0300F00F; - aVoxel = (aVoxel | (aVoxel << 4)) & 0x030C30C3; - aVoxel = (aVoxel | (aVoxel << 2)) & 0x09249249; - - aMortonCode |= (aVoxel << aCompIter); - } + // Compute voxel coordinates clamped to valid range + const Standard_Integer aVoxelX = + std::clamp(BVH::IntFloor(BVH::VecComp::Get(aVoxelF, 0)), 0, aDimension - 1); + const Standard_Integer aVoxelY = + std::clamp(BVH::IntFloor(BVH::VecComp::Get(aVoxelF, 1)), 0, aDimension - 1); + const Standard_Integer aVoxelZ = + (aNbEffComp > 2) + ? std::clamp(BVH::IntFloor(BVH::VecComp::Get(aVoxelF, 2)), 0, aDimension - 1) + : 0; + + // Use LUT-based Morton code encoding for better performance + const unsigned int aMortonCode = BVH::EncodeMortonCode(static_cast(aVoxelX), + static_cast(aVoxelY), + static_cast(aVoxelZ)); myEncodedLinks->ChangeValue(aPrimIdx) = BVH_EncodedLink(aMortonCode, aPrimIdx); } diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Ray.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Ray.hxx index 02b6ae1567..ce418f55b7 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Ray.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Ray.hxx @@ -16,6 +16,8 @@ #ifndef _BVH_Ray_Header #define _BVH_Ray_Header +#include + //! Describes a ray based on BVH vectors. template class BVH_Ray @@ -24,15 +26,23 @@ public: typedef typename BVH::VectorType::Type BVH_VecNt; public: - BVH_VecNt Origin; - BVH_VecNt Direct; + BVH_VecNt Origin; //!< Ray origin point + BVH_VecNt Direct; //!< Ray direction vector public: - BVH_Ray(const BVH_VecNt& theOrigin, const BVH_VecNt& theDirect) + //! Creates ray with given origin and direction. + constexpr BVH_Ray(const BVH_VecNt& theOrigin, const BVH_VecNt& theDirect) noexcept : Origin(theOrigin), Direct(theDirect) { } + + //! Default constructor (creates invalid ray at origin). + constexpr BVH_Ray() noexcept + : Origin(BVH_VecNt()), + Direct(BVH_VecNt()) + { + } }; #endif // _BVH_Ray_Header diff --git a/src/FoundationClasses/TKMath/BVH/BVH_SpatialMedianBuilder.hxx b/src/FoundationClasses/TKMath/BVH/BVH_SpatialMedianBuilder.hxx index e5dcd469ba..52ae3f07c2 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_SpatialMedianBuilder.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_SpatialMedianBuilder.hxx @@ -34,7 +34,7 @@ public: } //! Releases resources of spatial median split builder. - virtual ~BVH_SpatialMedianBuilder() {} + virtual ~BVH_SpatialMedianBuilder() = default; }; #endif // _BVH_SpatialMedianBuilder_Header diff --git a/src/FoundationClasses/TKMath/BVH/BVH_SweepPlaneBuilder.hxx b/src/FoundationClasses/TKMath/BVH/BVH_SweepPlaneBuilder.hxx index 9e030ab6b1..e74c23ff1a 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_SweepPlaneBuilder.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_SweepPlaneBuilder.hxx @@ -46,7 +46,7 @@ protected: const Standard_Integer aNodeBegPrimitive = theBVH->BegPrimitive(theNode); const Standard_Integer aNodeEndPrimitive = theBVH->EndPrimitive(theNode); const Standard_Integer aNodeNbPrimitives = theBVH->NbPrimitives(theNode); - if (aNodeEndPrimitive - aNodeBegPrimitive < BVH_Builder::myLeafNodeSize) + if (aNodeNbPrimitives <= BVH_Builder::myLeafNodeSize) { // clang-format off return typename BVH_QueueBuilder::BVH_ChildNodes(); // node does not require partitioning @@ -57,8 +57,8 @@ protected: Standard_Integer aMinSplitAxis = -1; Standard_Integer aMinSplitIndex = 0; - NCollection_Array1 aLftSet(0, aNodeNbPrimitives - 1); - NCollection_Array1 aRghSet(0, aNodeNbPrimitives - 1); + NCollection_Array1 aLftSet(1, aNodeNbPrimitives - 1); + NCollection_Array1 aRghSet(1, aNodeNbPrimitives - 1); Standard_Real aMinSplitCost = std::numeric_limits::max(); // Find best split @@ -74,8 +74,6 @@ protected: BVH_QuickSorter(anAxis).Perform(theSet, aNodeBegPrimitive, aNodeEndPrimitive); BVH_Box aLftBox; BVH_Box aRghBox; - aLftSet.ChangeFirst() = std::numeric_limits::max(); - aRghSet.ChangeFirst() = std::numeric_limits::max(); // Sweep from left for (Standard_Integer anIndex = 1; anIndex < aNodeNbPrimitives; ++anIndex) diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Tools.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Tools.hxx index 54d4bc0e26..eef6e2f4d3 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Tools.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Tools.hxx @@ -57,20 +57,18 @@ public: //! @name Box-Box Square distance const BVH_VecNt& theCMin2, const BVH_VecNt& theCMax2) { - T aDist = 0; + T aDist = T(0); for (int i = 0; i < N; ++i) { if (theCMin1[i] > theCMax2[i]) { T d = theCMin1[i] - theCMax2[i]; - d *= d; - aDist += d; + aDist += d * d; } else if (theCMax1[i] < theCMin2[i]) { T d = theCMin2[i] - theCMax1[i]; - d *= d; - aDist += d; + aDist += d * d; } } return aDist; @@ -92,20 +90,18 @@ public: //! @name Point-Box Square distance const BVH_VecNt& theCMin, const BVH_VecNt& theCMax) { - T aDist = 0; + T aDist = T(0); for (int i = 0; i < N; ++i) { if (thePoint[i] < theCMin[i]) { T d = theCMin[i] - thePoint[i]; - d *= d; - aDist += d; + aDist += d * d; } else if (thePoint[i] > theCMax[i]) { T d = thePoint[i] - theCMax[i]; - d *= d; - aDist += d; + aDist += d * d; } } return aDist; @@ -130,8 +126,49 @@ public: //! @name Point-Box projection return thePoint.cwiseMax(theCMin).cwiseMin(theCMax); } +private: //! @name Internal helpers for point-triangle projection + //! Helper to set projection state for vertex + static void SetVertexState(BVH_PrjStateInTriangle* thePrjState, + Standard_Integer* theFirstNode, + Standard_Integer* theLastNode, + Standard_Integer theVertexIndex) + { + if (thePrjState != nullptr) + { + *thePrjState = BVH_PrjStateInTriangle_VERTEX; + *theFirstNode = theVertexIndex; + *theLastNode = theVertexIndex; + } + } + + //! Helper to set projection state for edge + static void SetEdgeState(BVH_PrjStateInTriangle* thePrjState, + Standard_Integer* theFirstNode, + Standard_Integer* theLastNode, + Standard_Integer theStartVertex, + Standard_Integer theEndVertex) + { + if (thePrjState != nullptr) + { + *thePrjState = BVH_PrjStateInTriangle_EDGE; + *theFirstNode = theStartVertex; + *theLastNode = theEndVertex; + } + } + + //! Helper to compute projection onto edge + static BVH_VecNt ProjectToEdge(const BVH_VecNt& theEdgeStart, + const BVH_VecNt& theEdge, + T theDot1, + T theDot2) + { + T aT = theDot1 / (theDot1 + theDot2); + return theEdgeStart + theEdge * aT; + } + public: //! @name Point-Triangle Square distance - //! Find nearest point on a triangle for the given point + //! Find nearest point on a triangle for the given point. + //! Uses Voronoi region testing to determine closest feature (vertex, edge, or interior). static BVH_VecNt PointTriangleProjection(const BVH_VecNt& thePoint, const BVH_VecNt& theNode0, const BVH_VecNt& theNode1, @@ -140,101 +177,87 @@ public: //! @name Point-Triangle Square distance Standard_Integer* theNumberOfFirstNode = nullptr, Standard_Integer* theNumberOfLastNode = nullptr) { + // Compute edge vectors const BVH_VecNt aAB = theNode1 - theNode0; const BVH_VecNt aAC = theNode2 - theNode0; + const BVH_VecNt aBC = theNode2 - theNode1; + + // Compute point-to-vertex vectors const BVH_VecNt aAP = thePoint - theNode0; + const BVH_VecNt aBP = thePoint - theNode1; + const BVH_VecNt aCP = thePoint - theNode2; - T aABdotAP = aAB.Dot(aAP); - T aACdotAP = aAC.Dot(aAP); + // Compute dot products for Voronoi region tests + const T aABdotAP = aAB.Dot(aAP); + const T aACdotAP = aAC.Dot(aAP); - if (aABdotAP <= 0. && aACdotAP <= 0.) + // Check if P is in vertex region outside A + if (aABdotAP <= T(0) && aACdotAP <= T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_VERTEX; - *theNumberOfFirstNode = 0; - *theNumberOfLastNode = 0; - } + SetVertexState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 0); return theNode0; } - const BVH_VecNt aBC = theNode2 - theNode1; - const BVH_VecNt aBP = thePoint - theNode1; - - T aBAdotBP = -(aAB.Dot(aBP)); - T aBCdotBP = (aBC.Dot(aBP)); + const T aBAdotBP = -aAB.Dot(aBP); + const T aBCdotBP = aBC.Dot(aBP); - if (aBAdotBP <= 0. && aBCdotBP <= 0.) + // Check if P is in vertex region outside B + if (aBAdotBP <= T(0) && aBCdotBP <= T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_VERTEX; - *theNumberOfFirstNode = 1; - *theNumberOfLastNode = 1; - } + SetVertexState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 1); return theNode1; } - const BVH_VecNt aCP = thePoint - theNode2; - - T aCBdotCP = -(aBC.Dot(aCP)); - T aCAdotCP = -(aAC.Dot(aCP)); + const T aCBdotCP = -aBC.Dot(aCP); + const T aCAdotCP = -aAC.Dot(aCP); - if (aCAdotCP <= 0. && aCBdotCP <= 0.) + // Check if P is in vertex region outside C + if (aCAdotCP <= T(0) && aCBdotCP <= T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_VERTEX; - *theNumberOfFirstNode = 2; - *theNumberOfLastNode = 2; - } + SetVertexState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 2); return theNode2; } - T aACdotBP = (aAC.Dot(aBP)); - - T aVC = aABdotAP * aACdotBP + aBAdotBP * aACdotAP; + // Compute barycentric coordinates for edge/interior tests + const T aACdotBP = aAC.Dot(aBP); + const T aVC = aABdotAP * aACdotBP + aBAdotBP * aACdotAP; - if (aVC <= 0. && aABdotAP > 0. && aBAdotBP > 0.) + // Check if P is in edge region of AB + if (aVC <= T(0) && aABdotAP > T(0) && aBAdotBP > T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_EDGE; - *theNumberOfFirstNode = 0; - *theNumberOfLastNode = 1; - } - return theNode0 + aAB * (aABdotAP / (aABdotAP + aBAdotBP)); + SetEdgeState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 0, 1); + return ProjectToEdge(theNode0, aAB, aABdotAP, aBAdotBP); } - T aABdotCP = (aAB.Dot(aCP)); - - T aVA = aBAdotBP * aCAdotCP - aABdotCP * aACdotBP; + const T aABdotCP = aAB.Dot(aCP); + const T aVA = aBAdotBP * aCAdotCP - aABdotCP * aACdotBP; - if (aVA <= 0. && aBCdotBP > 0. && aCBdotCP > 0.) + // Check if P is in edge region of BC + if (aVA <= T(0) && aBCdotBP > T(0) && aCBdotCP > T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_EDGE; - *theNumberOfFirstNode = 1; - *theNumberOfLastNode = 2; - } - return theNode1 + aBC * (aBCdotBP / (aBCdotBP + aCBdotCP)); + SetEdgeState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 1, 2); + return ProjectToEdge(theNode1, aBC, aBCdotBP, aCBdotCP); } - T aVB = aABdotCP * aACdotAP + aABdotAP * aCAdotCP; + const T aVB = aABdotCP * aACdotAP + aABdotAP * aCAdotCP; - if (aVB <= 0. && aACdotAP > 0. && aCAdotCP > 0.) + // Check if P is in edge region of CA + if (aVB <= T(0) && aACdotAP > T(0) && aCAdotCP > T(0)) { - if (thePrjState != nullptr) - { - *thePrjState = BVH_PrjStateInTriangle_EDGE; - *theNumberOfFirstNode = 2; - *theNumberOfLastNode = 0; - } - return theNode0 + aAC * (aACdotAP / (aACdotAP + aCAdotCP)); + SetEdgeState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 2, 0); + return ProjectToEdge(theNode0, aAC, aACdotAP, aCAdotCP); } - T aNorm = aVA + aVB + aVC; + // P is inside triangle - compute barycentric coordinates + const T aNorm = aVA + aVB + aVC; + + // Handle degenerate triangle (zero or near-zero area) + if (aNorm + <= std::numeric_limits::epsilon() * (std::abs(aVA) + std::abs(aVB) + std::abs(aVC))) + { + SetVertexState(thePrjState, theNumberOfFirstNode, theNumberOfLastNode, 0); + return (theNode0 + theNode1 + theNode2) / T(3); + } if (thePrjState != nullptr) { @@ -273,7 +296,7 @@ public: //! @name Ray-Box Intersection theTimeLeave); } - //! Computes hit time of ray-box intersection + //! Computes hit time of ray-box intersection. static Standard_Boolean RayBoxIntersection(const BVH_Ray& theRay, const BVH_VecNt& theBoxCMin, const BVH_VecNt& theBoxCMax, @@ -307,7 +330,8 @@ public: //! @name Ray-Box Intersection theTimeLeave); } - //! Computes hit time of ray-box intersection + //! Computes hit time of ray-box intersection. + //! Uses optimized single-pass algorithm with early exit. static Standard_Boolean RayBoxIntersection(const BVH_VecNt& theRayOrigin, const BVH_VecNt& theRayDirection, const BVH_VecNt& theBoxCMin, @@ -315,41 +339,50 @@ public: //! @name Ray-Box Intersection T& theTimeEnter, T& theTimeLeave) { - BVH_VecNt aNodeMin, aNodeMax; + T aTimeEnter = (std::numeric_limits::lowest)(); + T aTimeLeave = (std::numeric_limits::max)(); + for (int i = 0; i < N; ++i) { - if (theRayDirection[i] == 0) + if (theRayDirection[i] == T(0)) { - aNodeMin[i] = (theBoxCMin[i] - theRayOrigin[i]) <= 0 ? (std::numeric_limits::min)() - : (std::numeric_limits::max)(); - aNodeMax[i] = (theBoxCMax[i] - theRayOrigin[i]) < 0 ? (std::numeric_limits::min)() - : (std::numeric_limits::max)(); + // Ray is parallel to this axis slab - check if origin is within bounds + if (theRayOrigin[i] < theBoxCMin[i] || theRayOrigin[i] > theBoxCMax[i]) + { + return Standard_False; // Ray misses the slab entirely + } + // Ray is within the slab, doesn't constrain the intersection interval + continue; } - else + + // Compute intersection distances for this axis + T aT1 = (theBoxCMin[i] - theRayOrigin[i]) / theRayDirection[i]; + T aT2 = (theBoxCMax[i] - theRayOrigin[i]) / theRayDirection[i]; + + // Ensure aT1 <= aT2 (handle negative direction) + T aTMin = (std::min)(aT1, aT2); + T aTMax = (std::max)(aT1, aT2); + + // Update intersection interval + aTimeEnter = (std::max)(aTimeEnter, aTMin); + aTimeLeave = (std::min)(aTimeLeave, aTMax); + + // Early exit if no intersection + if (aTimeEnter > aTimeLeave) { - aNodeMin[i] = (theBoxCMin[i] - theRayOrigin[i]) / theRayDirection[i]; - aNodeMax[i] = (theBoxCMax[i] - theRayOrigin[i]) / theRayDirection[i]; + return Standard_False; } } - BVH_VecNt aTimeMin, aTimeMax; - for (int i = 0; i < N; ++i) - { - aTimeMin[i] = (std::min)(aNodeMin[i], aNodeMax[i]); - aTimeMax[i] = (std::max)(aNodeMin[i], aNodeMax[i]); - } - - T aTimeEnter = (std::max)(aTimeMin[0], (std::max)(aTimeMin[1], aTimeMin[2])); - T aTimeLeave = (std::min)(aTimeMax[0], (std::min)(aTimeMax[1], aTimeMax[2])); - - Standard_Boolean hasIntersection = aTimeEnter <= aTimeLeave && aTimeLeave >= 0; - if (hasIntersection) + // Check if intersection is behind the ray origin + if (aTimeLeave < T(0)) { - theTimeEnter = aTimeEnter; - theTimeLeave = aTimeLeave; + return Standard_False; } - return hasIntersection; + theTimeEnter = aTimeEnter; + theTimeLeave = aTimeLeave; + return Standard_True; } }; diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Traverse.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Traverse.hxx index edcbd6bff0..0a6d706c47 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Traverse.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Traverse.hxx @@ -238,6 +238,23 @@ public: //! @name Selection //! Returns the number of accepted elements. Standard_Integer Select(const opencascade::handle>& theBVH); +protected: //! @name Internal structures + //! Auxiliary structure for keeping the nodes to process + struct BVH_NodeInStack + { + //! Constructor + constexpr BVH_NodeInStack(const Standard_Integer theNodeID = 0, + const MetricType& theMetric = MetricType()) noexcept + : NodeID(theNodeID), + Metric(theMetric) + { + } + + // Fields + Standard_Integer NodeID; //!< Id of the node in the BVH tree + MetricType Metric; //!< Metric computed for the node + }; + protected: //! @name Fields BVHSetType* myBVHSet; }; @@ -311,6 +328,26 @@ public: //! @name Selection Standard_Integer Select(const opencascade::handle>& theBVH1, const opencascade::handle>& theBVH2); +protected: //! @name Internal structures + //! Auxiliary structure for keeping the pair of nodes to process + struct BVH_PairNodesInStack + { + //! Constructor + constexpr BVH_PairNodesInStack(const Standard_Integer theNodeID1 = 0, + const Standard_Integer theNodeID2 = 0, + const MetricType& theMetric = MetricType()) noexcept + : NodeID1(theNodeID1), + NodeID2(theNodeID2), + Metric(theMetric) + { + } + + // Fields + Standard_Integer NodeID1; //!< Id of the node in the first BVH tree + Standard_Integer NodeID2; //!< Id of the node in the second BVH tree + MetricType Metric; //!< Metric computed for the pair of nodes + }; + protected: //! @name Fields BVHSetType* myBVHSet1; BVHSetType* myBVHSet2; diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Traverse.lxx b/src/FoundationClasses/TKMath/BVH/BVH_Traverse.lxx index ba79046b6b..6eb3649853 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Traverse.lxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Traverse.lxx @@ -13,24 +13,7 @@ // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. -namespace -{ -//! Auxiliary structure for keeping the nodes to process -template -struct BVH_NodeInStack -{ - //! Constructor - BVH_NodeInStack(const Standard_Integer theNodeID = 0, const MetricType& theMetric = MetricType()) - : NodeID(theNodeID), - Metric(theMetric) - { - } - - // Fields - Standard_Integer NodeID; //!< Id of the node in the BVH tree - MetricType Metric; //!< Metric computed for the node -}; -} // namespace +#include //================================================================================================= @@ -41,23 +24,24 @@ Standard_Integer BVH_Traverse::Selec if (theBVH.IsNull()) return 0; - if (theBVH->NodeInfoBuffer().empty()) + const BVH_Array4i& aBVHNodes = theBVH->NodeInfoBuffer(); + if (aBVHNodes.empty()) return 0; // Create stack - BVH_NodeInStack aStack[BVH_Constants_MaxTreeDepth]; + BVH_NodeInStack aStack[BVH_Constants_MaxTreeDepth]; // clang-format off - BVH_NodeInStack aNode (0); // Currently processed node, starting with the root node + BVH_NodeInStack aNode (0); // Currently processed node, starting with the root node // clang-format on - BVH_NodeInStack aPrevNode = aNode; // Previously processed node + BVH_NodeInStack aPrevNode = aNode; // Previously processed node Standard_Integer aHead = -1; // End of the stack Standard_Integer aNbAccepted = 0; // Counter for accepted elements for (;;) { - const BVH_Vec4i& aData = theBVH->NodeInfoBuffer()[aNode.NodeID]; + const BVH_Vec4i& aData = aBVHNodes[aNode.NodeID]; if (aData.x() == 0) { @@ -85,29 +69,32 @@ Standard_Integer BVH_Traverse::Selec { // Chose the branch with the best metric to be processed next, // put the other branch in the stack + Standard_ASSERT_RAISE(aHead < BVH_Constants_MaxTreeDepth - 1, + "Error! BVH stack overflow"); if (this->IsMetricBetter(aMetricLft, aMetricRgh)) { - aNode = BVH_NodeInStack(aData.y(), aMetricLft); - aStack[++aHead] = BVH_NodeInStack(aData.z(), aMetricRgh); + aNode = BVH_NodeInStack(aData.y(), aMetricLft); + aStack[++aHead] = BVH_NodeInStack(aData.z(), aMetricRgh); } else { - aNode = BVH_NodeInStack(aData.z(), aMetricRgh); - aStack[++aHead] = BVH_NodeInStack(aData.y(), aMetricLft); + aNode = BVH_NodeInStack(aData.z(), aMetricRgh); + aStack[++aHead] = BVH_NodeInStack(aData.y(), aMetricLft); } } else if (isGoodLft || isGoodRgh) { - aNode = isGoodLft ? BVH_NodeInStack(aData.y(), aMetricLft) - : BVH_NodeInStack(aData.z(), aMetricRgh); + aNode = isGoodLft ? BVH_NodeInStack(aData.y(), aMetricLft) + : BVH_NodeInStack(aData.z(), aMetricRgh); } } else { // Both children will be accepted // Take one for processing, put the other into stack - aNode = BVH_NodeInStack(aData.y(), aNode.Metric); - aStack[++aHead] = BVH_NodeInStack(aData.z(), aNode.Metric); + Standard_ASSERT_RAISE(aHead < BVH_Constants_MaxTreeDepth - 1, "Error! BVH stack overflow"); + aNode = BVH_NodeInStack(aData.y(), aNode.Metric); + aStack[++aHead] = BVH_NodeInStack(aData.z(), aNode.Metric); } } else @@ -142,29 +129,6 @@ Standard_Integer BVH_Traverse::Selec } } -namespace -{ -//! Auxiliary structure for keeping the pair of nodes to process -template -struct BVH_PairNodesInStack -{ - //! Constructor - BVH_PairNodesInStack(const Standard_Integer theNodeID1 = 0, - const Standard_Integer theNodeID2 = 0, - const MetricType& theMetric = MetricType()) - : NodeID1(theNodeID1), - NodeID2(theNodeID2), - Metric(theMetric) - { - } - - // Fields - Standard_Integer NodeID1; //!< Id of the node in the first BVH tree - Standard_Integer NodeID2; //!< Id of the node in the second BVH tree - MetricType Metric; //!< Metric computed for the pair of nodes -}; -} // namespace - //================================================================================================= template @@ -187,12 +151,12 @@ Standard_Integer BVH_PairTraverse::S const Standard_Integer aMaxNbPairsInStack = 3 * BVH_Constants_MaxTreeDepth; // Stack of pairs of nodes to process - BVH_PairNodesInStack aStack[aMaxNbPairsInStack]; + BVH_PairNodesInStack aStack[aMaxNbPairsInStack]; // Currently processed pair, starting with the root nodes - BVH_PairNodesInStack aNode(0, 0); + BVH_PairNodesInStack aNode(0, 0); // Previously processed pair - BVH_PairNodesInStack aPrevNode = aNode; + BVH_PairNodesInStack aPrevNode = aNode; // End of the stack Standard_Integer aHead = -1; // Counter for accepted elements @@ -205,47 +169,59 @@ Standard_Integer BVH_PairTraverse::S if (aData1.x() != 0 && aData2.x() != 0) { - // Outer/Outer - for (Standard_Integer iN1 = aData1.y(); iN1 <= aData1.z(); ++iN1) + // Outer/Outer - both nodes are leaves + // Check if the leaf node bounding boxes overlap before testing elements + MetricType aMetric; + Standard_Boolean isRejected = RejectNode(theBVH1->MinPoint(aNode.NodeID1), + theBVH1->MaxPoint(aNode.NodeID1), + theBVH2->MinPoint(aNode.NodeID2), + theBVH2->MaxPoint(aNode.NodeID2), + aMetric); + + if (!isRejected) { - for (Standard_Integer iN2 = aData2.y(); iN2 <= aData2.z(); ++iN2) + // Bounding boxes overlap, test all element pairs + for (Standard_Integer iN1 = aData1.y(); iN1 <= aData1.z(); ++iN1) { - if (Accept(iN1, iN2)) - ++aNbAccepted; + for (Standard_Integer iN2 = aData2.y(); iN2 <= aData2.z(); ++iN2) + { + if (Accept(iN1, iN2)) + ++aNbAccepted; - if (this->Stop()) - return aNbAccepted; + if (this->Stop()) + return aNbAccepted; + } } } } else { - BVH_PairNodesInStack aPairs[4]; - Standard_Integer aNbPairs = 0; + BVH_PairNodesInStack aPairs[4]; + Standard_Integer aNbPairs = 0; if (aData1.x() == 0 && aData2.x() == 0) { // Inner/Inner - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aData2.y()); - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aData2.z()); - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aData2.y()); - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aData2.z()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aData2.y()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aData2.z()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aData2.y()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aData2.z()); } else if (aData1.x() == 0) { // Inner/Outer - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aNode.NodeID2); - aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aNode.NodeID2); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.y(), aNode.NodeID2); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aData1.z(), aNode.NodeID2); } else if (aData2.x() == 0) { // Outer/Inner - aPairs[aNbPairs++] = BVH_PairNodesInStack(aNode.NodeID1, aData2.y()); - aPairs[aNbPairs++] = BVH_PairNodesInStack(aNode.NodeID1, aData2.z()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aNode.NodeID1, aData2.y()); + aPairs[aNbPairs++] = BVH_PairNodesInStack(aNode.NodeID1, aData2.z()); } - BVH_PairNodesInStack aKeptPairs[4]; - Standard_Integer aNbKept = 0; + BVH_PairNodesInStack aKeptPairs[4]; + Standard_Integer aNbKept = 0; // Compute metrics for the nodes for (Standard_Integer iPair = 0; iPair < aNbPairs; ++iPair) { @@ -265,7 +241,7 @@ Standard_Integer BVH_PairTraverse::S --iSort; } aKeptPairs[iSort] = aPairs[iPair]; - aNbKept++; + ++aNbKept; } } @@ -275,6 +251,7 @@ Standard_Integer BVH_PairTraverse::S for (Standard_Integer iPair = 1; iPair < aNbKept; ++iPair) { + Standard_ASSERT_RAISE(aHead < aMaxNbPairsInStack - 1, "Error! BVH pair stack overflow"); aStack[++aHead] = aKeptPairs[iPair]; } } diff --git a/src/FoundationClasses/TKMath/BVH/BVH_Types.hxx b/src/FoundationClasses/TKMath/BVH/BVH_Types.hxx index 02aa40e1c8..f2e3f28507 100644 --- a/src/FoundationClasses/TKMath/BVH/BVH_Types.hxx +++ b/src/FoundationClasses/TKMath/BVH/BVH_Types.hxx @@ -188,7 +188,7 @@ struct VecComp { typedef typename BVH::VectorType::Type BVH_Vec2t; - static T Get(const BVH_Vec2t& theVec, const Standard_Integer theAxis) + static constexpr T Get(const BVH_Vec2t& theVec, const Standard_Integer theAxis) { return theAxis == 0 ? theVec.x() : theVec.y(); } @@ -199,7 +199,7 @@ struct VecComp { typedef typename BVH::VectorType::Type BVH_Vec3t; - static T Get(const BVH_Vec3t& theVec, const Standard_Integer theAxis) + static constexpr T Get(const BVH_Vec3t& theVec, const Standard_Integer theAxis) { return theAxis == 0 ? theVec.x() : (theAxis == 1 ? theVec.y() : theVec.z()); } @@ -210,7 +210,7 @@ struct VecComp { typedef typename BVH::VectorType::Type BVH_Vec4t; - static T Get(const BVH_Vec4t& theVec, const Standard_Integer theAxis) + static constexpr T Get(const BVH_Vec4t& theVec, const Standard_Integer theAxis) { return theAxis == 0 ? theVec.x() : (theAxis == 1 ? theVec.y() : (theAxis == 2 ? theVec.z() : theVec.w())); @@ -302,7 +302,7 @@ struct Array }; template -static inline Standard_Integer IntFloor(const T theValue) +static inline constexpr Standard_Integer IntFloor(const T theValue) { const Standard_Integer aRes = static_cast(theValue); diff --git a/src/FoundationClasses/TKMath/BVH/FILES.cmake b/src/FoundationClasses/TKMath/BVH/FILES.cmake index 5276ccdad9..a870df7d93 100644 --- a/src/FoundationClasses/TKMath/BVH/FILES.cmake +++ b/src/FoundationClasses/TKMath/BVH/FILES.cmake @@ -9,7 +9,6 @@ set(OCCT_BVH_FILES BVH_Builder.hxx BVH_Builder3d.hxx BVH_BuildQueue.hxx - BVH_BuildQueue.cxx BVH_BuildThread.hxx BVH_BuildThread.cxx BVH_Constants.hxx diff --git a/src/FoundationClasses/TKMath/GTests/BVH_BinnedBuilder_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_BinnedBuilder_Test.cxx new file mode 100644 index 0000000000..9d68227b2b --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_BinnedBuilder_Test.cxx @@ -0,0 +1,307 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include +#include + +TEST(BVH_BinnedBuilderTest, DefaultConstructor) +{ + BVH_BinnedBuilder aBuilder; + + EXPECT_GT(aBuilder.LeafNodeSize(), 0); + EXPECT_GT(aBuilder.MaxTreeDepth(), 0); +} + +TEST(BVH_BinnedBuilderTest, CustomParameters) +{ + BVH_BinnedBuilder aBuilder(5, 20); + + EXPECT_EQ(aBuilder.LeafNodeSize(), 5); + EXPECT_EQ(aBuilder.MaxTreeDepth(), 20); +} + +TEST(BVH_BinnedBuilderTest, BuildEmptySet) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(); + BVH_BoxSet aBoxSet(aBuilder); + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_EQ(aBVH->Length(), 0); +} + +TEST(BVH_BinnedBuilderTest, BuildSingleElement) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(); + BVH_BoxSet aBoxSet(aBuilder); + + aBoxSet.Add(0, BVH_Box(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0))); + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_EQ(aBVH->Length(), 1); + EXPECT_TRUE(aBVH->IsOuter(0)); +} + +TEST(BVH_BinnedBuilderTest, BuildMultipleElements) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes along X axis + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + EXPECT_GT(aBVH->Depth(), 0); +} + +TEST(BVH_BinnedBuilderTest, SAHOptimization) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes in a pattern where SAH should matter + // Two clusters: one near origin, one far away + for (int i = 0; i < 5; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 0.1, 0.0, 0.0), + BVH_Vec3d(i * 0.1 + 0.1, 0.1, 0.1)); + aBoxSet.Add(i, aBox); + } + + for (int i = 0; i < 5; ++i) + { + BVH_Box aBox(BVH_Vec3d(100.0 + i * 0.1, 0.0, 0.0), + BVH_Vec3d(100.0 + i * 0.1 + 0.1, 0.1, 0.1)); + aBoxSet.Add(i + 5, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + + // SAH should produce a reasonable tree + Standard_Real aSAH = aBVH->EstimateSAH(); + EXPECT_GT(aSAH, 0.0); +} + +TEST(BVH_BinnedBuilderTest, LeafNodeSizeRespected) +{ + const int aLeafSize = 3; + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(aLeafSize, 32); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + + // Check that leaf nodes don't exceed leaf size + for (int i = 0; i < aBVH->Length(); ++i) + { + if (aBVH->IsOuter(i)) + { + int aNbPrims = aBVH->NbPrimitives(i); + EXPECT_LE(aNbPrims, aLeafSize); + } + } +} + +TEST(BVH_BinnedBuilderTest, BuildWithDifferentBinCounts) +{ + // Test with different number of bins (template parameter) + opencascade::handle> aBuilder16 = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder16); + + for (int i = 0; i < 20; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 1.0, 0.0, 0.0), + BVH_Vec3d(i * 1.0 + 0.5, 0.5, 0.5)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} + +TEST(BVH_BinnedBuilderTest, Build2D) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec2d(i * 2.0, 0.0), BVH_Vec2d(i * 2.0 + 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} + +// Note: Float tests skipped due to BVH_BoxSet::Center return type issue + +TEST(BVH_BinnedBuilderTest, RandomDistribution) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes in a 3D grid pattern + int aCount = 0; + for (int x = 0; x < 5; ++x) + { + for (int y = 0; y < 5; ++y) + { + for (int z = 0; z < 5; ++z) + { + BVH_Box aBox(BVH_Vec3d(x * 2.0, y * 2.0, z * 2.0), + BVH_Vec3d(x * 2.0 + 1.0, y * 2.0 + 1.0, z * 2.0 + 1.0)); + aBoxSet.Add(aCount++, aBox); + } + } + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + + // Verify tree covers all primitives + int aTotalPrims = 0; + for (int i = 0; i < aBVH->Length(); ++i) + { + if (aBVH->IsOuter(i)) + { + aTotalPrims += aBVH->NbPrimitives(i); + } + } + EXPECT_EQ(aTotalPrims, 125); +} + +TEST(BVH_BinnedBuilderTest, CompareTreeQuality) +{ + // Build tree with small leaf size vs large leaf size + opencascade::handle> aBuilder1 = + new BVH_BinnedBuilder(1, 32); + opencascade::handle> aBuilder4 = + new BVH_BinnedBuilder(4, 32); + + BVH_BoxSet aBoxSet1(aBuilder1); + BVH_BoxSet aBoxSet4(aBuilder4); + + for (int i = 0; i < 50; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet1.Add(i, aBox); + aBoxSet4.Add(i, aBox); + } + + aBoxSet1.Build(); + aBoxSet4.Build(); + + const opencascade::handle>& aBVH1 = aBoxSet1.BVH(); + const opencascade::handle>& aBVH4 = aBoxSet4.BVH(); + + // Tree with smaller leaf size should be deeper + EXPECT_GE(aBVH1->Depth(), aBVH4->Depth()); +} + +TEST(BVH_BinnedBuilderTest, MaxDepthRespected) +{ + const int aMaxDepth = 5; + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, aMaxDepth); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 100; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_LE(aBVH->Depth(), aMaxDepth); +} + +TEST(BVH_BinnedBuilderTest, OverlappingBoxes) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add overlapping boxes + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 0.5, 0.0, 0.0), + BVH_Vec3d(i * 0.5 + 2.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} + +TEST(BVH_BinnedBuilderTest, IdenticalBoxes) +{ + opencascade::handle> aBuilder = + new BVH_BinnedBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add identical boxes + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GE(aBVH->Length(), 1); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Box_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Box_Test.cxx new file mode 100644 index 0000000000..c05d57eaf2 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Box_Test.cxx @@ -0,0 +1,1045 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include + +TEST(BVH_BoxTest, DefaultConstructor) +{ + BVH_Box aBox; + EXPECT_FALSE(aBox.IsValid()); +} + +TEST(BVH_BoxTest, ConstructorWithCorners) +{ + BVH_Vec3d aMin(0.0, 0.0, 0.0); + BVH_Vec3d aMax(1.0, 2.0, 3.0); + + BVH_Box aBox(aMin, aMax); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 3.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Add) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec3d(1.0, 2.0, 3.0)); + EXPECT_TRUE(aBox.IsValid()); + + aBox.Add(BVH_Vec3d(-1.0, -2.0, -3.0)); + + EXPECT_NEAR(aBox.CornerMin().x(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), -2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), -3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 3.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Combine) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(3.0, 3.0, 3.0)); + + aBox1.Combine(aBox2); + + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMin().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMin().z(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().y(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().z(), 3.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Size) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(2.0, 3.0, 4.0)); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 4.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Center) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(2.0, 4.0, 6.0)); + + BVH_Vec3d aCenter = aBox.Center(); + EXPECT_NEAR(aCenter.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.z(), 3.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Area) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 2.0, 3.0)); + + // Surface area = 2 * (1*2 + 2*3 + 1*3) = 2 * (2 + 6 + 3) = 22 + Standard_Real anArea = aBox.Area(); + EXPECT_NEAR(anArea, 22.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, IsOut) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(3.0, 3.0, 3.0)); + BVH_Box aBox3(BVH_Vec3d(0.5, 0.5, 0.5), BVH_Vec3d(1.5, 1.5, 1.5)); + + EXPECT_TRUE(aBox1.IsOut(aBox2)); + EXPECT_FALSE(aBox1.IsOut(aBox3)); +} + +TEST(BVH_BoxTest, Clear) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + EXPECT_TRUE(aBox.IsValid()); + + aBox.Clear(); + EXPECT_FALSE(aBox.IsValid()); +} + +TEST(BVH_BoxTest, Box2D) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec2d(0.0, 0.0)); + aBox.Add(BVH_Vec2d(1.0, 1.0)); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 1.0, Precision::Confusion()); + + // Area in 2D is width * height (actual area) + Standard_Real anArea = aBox.Area(); + EXPECT_NEAR(anArea, 1.0, Precision::Confusion()); // 1 * 1 = 1 +} + +TEST(BVH_BoxTest, Box4D) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec4d(0.0, 0.0, 0.0, 0.0)); + aBox.Add(BVH_Vec4d(1.0, 2.0, 3.0, 4.0)); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().w(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().w(), 4.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, FloatPrecision) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec3f(0.0f, 0.0f, 0.0f)); + aBox.Add(BVH_Vec3f(1.0f, 2.0f, 3.0f)); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0f, 1e-5f); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0f, 1e-5f); + EXPECT_NEAR(aBox.CornerMax().y(), 2.0f, 1e-5f); + EXPECT_NEAR(aBox.CornerMax().z(), 3.0f, 1e-5f); +} + +TEST(BVH_BoxTest, SinglePointBox) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec3d(5.0, 5.0, 5.0)); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 5.0, Precision::Confusion()); + + // Size should be zero + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 0.0, Precision::Confusion()); + + // Area should be zero + EXPECT_NEAR(aBox.Area(), 0.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, NegativeCoordinates) +{ + BVH_Box aBox(BVH_Vec3d(-10.0, -20.0, -30.0), BVH_Vec3d(-5.0, -10.0, -15.0)); + + EXPECT_TRUE(aBox.IsValid()); + + BVH_Vec3d aCenter = aBox.Center(); + EXPECT_NEAR(aCenter.x(), -7.5, Precision::Confusion()); + EXPECT_NEAR(aCenter.y(), -15.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.z(), -22.5, Precision::Confusion()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 15.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, LargeValues) +{ + BVH_Box aBox(BVH_Vec3d(1e10, 1e10, 1e10), + BVH_Vec3d(1e10 + 1.0, 1e10 + 2.0, 1e10 + 3.0)); + + EXPECT_TRUE(aBox.IsValid()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 1.0, 1e-5); + EXPECT_NEAR(aSize.y(), 2.0, 1e-5); + EXPECT_NEAR(aSize.z(), 3.0, 1e-5); +} + +TEST(BVH_BoxTest, CombineWithInvalid) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2; // Invalid box + + aBox1.Combine(aBox2); + + // Box1 should remain unchanged when combining with invalid box + EXPECT_TRUE(aBox1.IsValid()); + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().x(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, AddToInvalid) +{ + BVH_Box aBox1; // Invalid box + BVH_Box aBox2(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + aBox1.Combine(aBox2); + + // Box1 should now be valid and equal to aBox2 + EXPECT_TRUE(aBox1.IsValid()); + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().x(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, IsOutTouchingBoxes) +{ + // Boxes that touch at a face + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(1.0, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0)); + + // Touching boxes should NOT be "out" + EXPECT_FALSE(aBox1.IsOut(aBox2)); +} + +TEST(BVH_BoxTest, IsOutTouchingAtEdge) +{ + // Boxes that touch at an edge + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(1.0, 1.0, 0.0), BVH_Vec3d(2.0, 2.0, 1.0)); + + // Touching at edge should NOT be "out" + EXPECT_FALSE(aBox1.IsOut(aBox2)); +} + +TEST(BVH_BoxTest, IsOutTouchingAtCorner) +{ + // Boxes that touch at a corner + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(1.0, 1.0, 1.0), BVH_Vec3d(2.0, 2.0, 2.0)); + + // Touching at corner should NOT be "out" + EXPECT_FALSE(aBox1.IsOut(aBox2)); +} + +TEST(BVH_BoxTest, AreaUnitCube) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Surface area of unit cube = 6 faces * 1 = 6 + // Formula: 2 * (xy + yz + xz) = 2 * (1 + 1 + 1) = 6 + EXPECT_NEAR(aBox.Area(), 6.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, CenterAtOrigin) +{ + BVH_Box aBox(BVH_Vec3d(-1.0, -1.0, -1.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + BVH_Vec3d aCenter = aBox.Center(); + EXPECT_NEAR(aCenter.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, MultipleAdds) +{ + BVH_Box aBox; + + // Add points in random order + aBox.Add(BVH_Vec3d(5.0, 3.0, 1.0)); + aBox.Add(BVH_Vec3d(-2.0, 7.0, 4.0)); + aBox.Add(BVH_Vec3d(1.0, -1.0, 8.0)); + aBox.Add(BVH_Vec3d(0.0, 2.0, -3.0)); + + EXPECT_NEAR(aBox.CornerMin().x(), -2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), -3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 7.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 8.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, FlatBox2D) +{ + // Box with zero extent in Z (flat) + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 5.0), BVH_Vec3d(3.0, 4.0, 5.0)); + + EXPECT_TRUE(aBox.IsValid()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 4.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 0.0, Precision::Confusion()); + + // Area = 2 * (3*4 + 4*0 + 3*0) = 24 + EXPECT_NEAR(aBox.Area(), 24.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, FlatBox1D) +{ + // Box with zero extent in Y and Z (line) + BVH_Box aBox(BVH_Vec3d(0.0, 5.0, 5.0), BVH_Vec3d(10.0, 5.0, 5.0)); + + EXPECT_TRUE(aBox.IsValid()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 0.0, Precision::Confusion()); + + // For degenerate box (line), Area returns length of longest dimension + // This is an implementation detail - the surface area formula gives 0, + // but Area() returns the length (10) for degenerate cases + Standard_Real anArea = aBox.Area(); + EXPECT_GE(anArea, 0.0); +} + +TEST(BVH_BoxTest, CombineMultiple) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 1.0)); + BVH_Box aBox3(BVH_Vec3d(0.0, 2.0, 0.0), BVH_Vec3d(1.0, 3.0, 1.0)); + + aBox1.Combine(aBox2); + aBox1.Combine(aBox3); + + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMin().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMin().z(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().y(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().z(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, IsOutPartialOverlap) +{ + // Boxes that partially overlap + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(2.0, 2.0, 2.0)); + BVH_Box aBox2(BVH_Vec3d(1.0, 1.0, 1.0), BVH_Vec3d(3.0, 3.0, 3.0)); + + EXPECT_FALSE(aBox1.IsOut(aBox2)); + EXPECT_FALSE(aBox2.IsOut(aBox1)); +} + +TEST(BVH_BoxTest, IsOutContained) +{ + // One box completely inside another + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(3.0, 3.0, 3.0)); + + EXPECT_FALSE(aBox1.IsOut(aBox2)); + EXPECT_FALSE(aBox2.IsOut(aBox1)); +} + +TEST(BVH_BoxTest, IsOutFloat) +{ + BVH_Box aBox1(BVH_Vec3f(0.0f, 0.0f, 0.0f), BVH_Vec3f(1.0f, 1.0f, 1.0f)); + BVH_Box aBox2(BVH_Vec3f(2.0f, 2.0f, 2.0f), BVH_Vec3f(3.0f, 3.0f, 3.0f)); + BVH_Box aBox3(BVH_Vec3f(0.5f, 0.5f, 0.5f), BVH_Vec3f(1.5f, 1.5f, 1.5f)); + + EXPECT_TRUE(aBox1.IsOut(aBox2)); + EXPECT_FALSE(aBox1.IsOut(aBox3)); +} + +TEST(BVH_BoxTest, Box2DIsOut) +{ + BVH_Box aBox1(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + BVH_Box aBox2(BVH_Vec2d(2.0, 2.0), BVH_Vec2d(3.0, 3.0)); + BVH_Box aBox3(BVH_Vec2d(0.5, 0.5), BVH_Vec2d(1.5, 1.5)); + + EXPECT_TRUE(aBox1.IsOut(aBox2)); + EXPECT_FALSE(aBox1.IsOut(aBox3)); +} + +TEST(BVH_BoxTest, Box2DCenter) +{ + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(4.0, 6.0)); + + BVH_Vec2d aCenter = aBox.Center(); + EXPECT_NEAR(aCenter.x(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.y(), 3.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Box2DSize) +{ + BVH_Box aBox(BVH_Vec2d(1.0, 2.0), BVH_Vec2d(4.0, 7.0)); + + BVH_Vec2d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 5.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, VerySmallBox) +{ + // Test with very small values + Standard_Real aSmall = 1e-10; + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(aSmall, aSmall, aSmall)); + + EXPECT_TRUE(aBox.IsValid()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), aSmall, 1e-15); + EXPECT_NEAR(aSize.y(), aSmall, 1e-15); + EXPECT_NEAR(aSize.z(), aSmall, 1e-15); +} + +TEST(BVH_BoxTest, SymmetricBox) +{ + BVH_Box aBox(BVH_Vec3d(-5.0, -5.0, -5.0), BVH_Vec3d(5.0, 5.0, 5.0)); + + BVH_Vec3d aCenter = aBox.Center(); + EXPECT_NEAR(aCenter.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aCenter.z(), 0.0, Precision::Confusion()); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 10.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, NonCubicBox) +{ + // Box with different dimensions on each axis + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 10.0, 100.0)); + + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 100.0, Precision::Confusion()); + + // Area = 2 * (1*10 + 10*100 + 1*100) = 2 * (10 + 1000 + 100) = 2220 + EXPECT_NEAR(aBox.Area(), 2220.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, ClearAndReuse) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + EXPECT_TRUE(aBox.IsValid()); + + aBox.Clear(); + EXPECT_FALSE(aBox.IsValid()); + + // Reuse the box + aBox.Add(BVH_Vec3d(5.0, 5.0, 5.0)); + aBox.Add(BVH_Vec3d(10.0, 10.0, 10.0)); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 10.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, IsOutSameBox) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Box should not be "out" of itself + EXPECT_FALSE(aBox.IsOut(aBox)); +} + +TEST(BVH_BoxTest, CombineSameBox) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + aBox1.Combine(aBox2); + + // Should remain unchanged + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox1.CornerMax().x(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, IsOutNearMiss) +{ + // Boxes that are very close but not touching + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(1.0001, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0)); + + EXPECT_TRUE(aBox1.IsOut(aBox2)); +} + +TEST(BVH_BoxTest, AddDuplicatePoint) +{ + BVH_Box aBox; + + aBox.Add(BVH_Vec3d(1.0, 2.0, 3.0)); + aBox.Add(BVH_Vec3d(1.0, 2.0, 3.0)); + aBox.Add(BVH_Vec3d(1.0, 2.0, 3.0)); + + // Should still be a single point box + EXPECT_NEAR(aBox.CornerMin().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Size().x(), 0.0, Precision::Confusion()); +} + +// ======================================================================================= +// Tests for Single-Point Constructor +// ======================================================================================= + +TEST(BVH_BoxTest, SinglePointConstructor) +{ + BVH_Vec3d aPoint(5.0, 10.0, 15.0); + BVH_Box aBox(aPoint); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), 15.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 15.0, Precision::Confusion()); + + // Size should be zero + BVH_Vec3d aSize = aBox.Size(); + EXPECT_NEAR(aSize.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aSize.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aSize.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, SinglePointConstructor2D) +{ + BVH_Vec2d aPoint(3.0, 7.0); + BVH_Box aBox(aPoint); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.CornerMin().x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 7.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 7.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, SinglePointConstructorOrigin) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Box aBox(aOrigin); + + EXPECT_TRUE(aBox.IsValid()); + EXPECT_NEAR(aBox.Center().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center().y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center().z(), 0.0, Precision::Confusion()); +} + +// ======================================================================================= +// Tests for Center(axis) method +// ======================================================================================= + +TEST(BVH_BoxTest, CenterByAxis) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 20.0, 30.0)); + + EXPECT_NEAR(aBox.Center(0), 5.0, Precision::Confusion()); // X axis + EXPECT_NEAR(aBox.Center(1), 10.0, Precision::Confusion()); // Y axis + EXPECT_NEAR(aBox.Center(2), 15.0, Precision::Confusion()); // Z axis +} + +TEST(BVH_BoxTest, CenterByAxis2D) +{ + BVH_Box aBox(BVH_Vec2d(2.0, 4.0), BVH_Vec2d(8.0, 12.0)); + + EXPECT_NEAR(aBox.Center(0), 5.0, Precision::Confusion()); // X axis + EXPECT_NEAR(aBox.Center(1), 8.0, Precision::Confusion()); // Y axis +} + +TEST(BVH_BoxTest, CenterByAxisNegative) +{ + BVH_Box aBox(BVH_Vec3d(-10.0, -20.0, -30.0), BVH_Vec3d(10.0, 20.0, 30.0)); + + EXPECT_NEAR(aBox.Center(0), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center(1), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center(2), 0.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, CenterByAxis4D) +{ + BVH_Box aBox(BVH_Vec4d(0.0, 0.0, 0.0, 0.0), BVH_Vec4d(4.0, 6.0, 8.0, 10.0)); + + EXPECT_NEAR(aBox.Center(0), 2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center(1), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.Center(2), 4.0, Precision::Confusion()); +} + +// ======================================================================================= +// Tests for IsOut(point) method +// ======================================================================================= + +TEST(BVH_BoxTest, IsOutPoint_Inside) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Point inside + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(5.0, 5.0, 5.0))); + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(1.0, 1.0, 1.0))); + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(9.0, 9.0, 9.0))); +} + +TEST(BVH_BoxTest, IsOutPoint_Outside) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Points outside + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(11.0, 5.0, 5.0))); // Outside X + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(5.0, 11.0, 5.0))); // Outside Y + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(5.0, 5.0, 11.0))); // Outside Z + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(-1.0, 5.0, 5.0))); // Outside negative X + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(15.0, 15.0, 15.0))); // Far outside +} + +TEST(BVH_BoxTest, IsOutPoint_OnBoundary) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Points on boundary (should be considered inside) + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(0.0, 5.0, 5.0))); // On min X face + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(10.0, 5.0, 5.0))); // On max X face + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(5.0, 0.0, 5.0))); // On min Y face + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(5.0, 10.0, 5.0))); // On max Y face + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(0.0, 0.0, 0.0))); // At corner + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(10.0, 10.0, 10.0))); // At opposite corner +} + +TEST(BVH_BoxTest, IsOutPoint_InvalidBox) +{ + BVH_Box aBox; // Invalid box + + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(0.0, 0.0, 0.0))); + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(5.0, 5.0, 5.0))); +} + +TEST(BVH_BoxTest, IsOutPoint_2D) +{ + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(5.0, 5.0)); + + EXPECT_FALSE(aBox.IsOut(BVH_Vec2d(2.5, 2.5))); // Inside + EXPECT_TRUE(aBox.IsOut(BVH_Vec2d(6.0, 2.5))); // Outside X + EXPECT_TRUE(aBox.IsOut(BVH_Vec2d(2.5, 6.0))); // Outside Y + EXPECT_FALSE(aBox.IsOut(BVH_Vec2d(0.0, 0.0))); // On corner +} + +TEST(BVH_BoxTest, IsOutPoint_NegativeCoords) +{ + BVH_Box aBox(BVH_Vec3d(-5.0, -5.0, -5.0), BVH_Vec3d(5.0, 5.0, 5.0)); + + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(0.0, 0.0, 0.0))); // At center + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(-3.0, -3.0, -3.0))); // Inside negative + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(-6.0, 0.0, 0.0))); // Outside negative X +} + +// ======================================================================================= +// Tests for Contains() methods +// ======================================================================================= + +TEST(BVH_BoxTest, Contains_FullyContained) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(8.0, 8.0, 8.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_TRUE(isContained); + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_NotContained) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(8.0, 8.0, 8.0), BVH_Vec3d(12.0, 12.0, 12.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_FALSE(isContained); + EXPECT_TRUE(hasOverlap); // They overlap but box2 is not fully contained +} + +TEST(BVH_BoxTest, Contains_NoOverlap) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(5.0, 5.0, 5.0)); + BVH_Box aBox2(BVH_Vec3d(10.0, 10.0, 10.0), BVH_Vec3d(15.0, 15.0, 15.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_FALSE(isContained); + EXPECT_FALSE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_SameBox) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_TRUE(isContained); + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_TouchingBoundary) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(5.0, 5.0, 5.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_TRUE(isContained); + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_InvalidBox) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2; // Invalid + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_FALSE(isContained); + EXPECT_FALSE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_InvalidContainer) +{ + BVH_Box aBox1; // Invalid + BVH_Box aBox2(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_FALSE(isContained); + EXPECT_FALSE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_PartialOverlapOneAxis) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(5.0, 5.0, 5.0), BVH_Vec3d(15.0, 8.0, 8.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_FALSE(isContained); // Not fully contained (extends in X) + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, Contains_2D) +{ + BVH_Box aBox1(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(10.0, 10.0)); + BVH_Box aBox2(BVH_Vec2d(2.0, 2.0), BVH_Vec2d(8.0, 8.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = aBox1.Contains(aBox2, hasOverlap); + + EXPECT_TRUE(isContained); + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, ContainsByCorners_FullyContained) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = + aBox.Contains(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(8.0, 8.0, 8.0), hasOverlap); + + EXPECT_TRUE(isContained); + EXPECT_TRUE(hasOverlap); +} + +TEST(BVH_BoxTest, ContainsByCorners_NotContained) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + Standard_Boolean hasOverlap = Standard_False; + Standard_Boolean isContained = + aBox.Contains(BVH_Vec3d(8.0, 8.0, 8.0), BVH_Vec3d(12.0, 12.0, 12.0), hasOverlap); + + EXPECT_FALSE(isContained); + EXPECT_TRUE(hasOverlap); +} + +// ======================================================================================= +// Tests for Transform() and Transformed() methods +// ======================================================================================= + +TEST(BVH_BoxTest, Transform_Identity) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + NCollection_Mat4 aIdentity; + aIdentity.InitIdentity(); + + aBox.Transform(aIdentity); + + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Transform_Translation) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + NCollection_Mat4 aTransform; + aTransform.InitIdentity(); + aTransform.SetColumn(3, NCollection_Vec3(5.0, 10.0, 15.0)); + + aBox.Transform(aTransform); + + EXPECT_NEAR(aBox.CornerMin().x(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), 15.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 6.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 11.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 16.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Transform_Scale) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + NCollection_Mat4 aTransform; + aTransform.InitIdentity(); + aTransform.SetValue(0, 0, 2.0); // Scale X by 2 + aTransform.SetValue(1, 1, 3.0); // Scale Y by 3 + aTransform.SetValue(2, 2, 4.0); // Scale Z by 4 + + aBox.Transform(aTransform); + + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 4.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Transformed_Identity) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + NCollection_Mat4 aIdentity; + aIdentity.InitIdentity(); + + BVH_Box aTransformed = aBox.Transformed(aIdentity); + + // Original should be unchanged + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + + // Transformed should be the same + EXPECT_NEAR(aTransformed.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMax().x(), 1.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Transformed_Translation) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + NCollection_Mat4 aTransform; + aTransform.InitIdentity(); + aTransform.SetColumn(3, NCollection_Vec3(10.0, 20.0, 30.0)); + + BVH_Box aTransformed = aBox.Transformed(aTransform); + + // Original should be unchanged + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, Precision::Confusion()); + + // Transformed should be translated + EXPECT_NEAR(aTransformed.CornerMin().x(), 10.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMin().y(), 20.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMin().z(), 30.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMax().x(), 11.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMax().y(), 21.0, Precision::Confusion()); + EXPECT_NEAR(aTransformed.CornerMax().z(), 31.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, Transform_InvalidBox) +{ + BVH_Box aBox; // Invalid box + + NCollection_Mat4 aTransform; + aTransform.InitIdentity(); + aTransform.SetColumn(3, NCollection_Vec3(10.0, 20.0, 30.0)); + + aBox.Transform(aTransform); + + // Should remain invalid + EXPECT_FALSE(aBox.IsValid()); +} + +TEST(BVH_BoxTest, Transformed_InvalidBox) +{ + BVH_Box aBox; // Invalid box + + NCollection_Mat4 aTransform; + aTransform.InitIdentity(); + aTransform.SetColumn(3, NCollection_Vec3(10.0, 20.0, 30.0)); + + BVH_Box aTransformed = aBox.Transformed(aTransform); + + // Should remain invalid + EXPECT_FALSE(aTransformed.IsValid()); +} + +// ======================================================================================= +// Tests for Corner Modification +// ======================================================================================= + +TEST(BVH_BoxTest, ModifyCornerMin) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Modify via non-const reference + aBox.CornerMin() = BVH_Vec3d(-5.0, -5.0, -5.0); + + EXPECT_NEAR(aBox.CornerMin().x(), -5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), -5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), -5.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().x(), 10.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, ModifyCornerMax) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Modify via non-const reference + aBox.CornerMax() = BVH_Vec3d(20.0, 30.0, 40.0); + + EXPECT_NEAR(aBox.CornerMax().x(), 20.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().y(), 30.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 40.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, Precision::Confusion()); +} + +TEST(BVH_BoxTest, ModifyCornerComponents) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Modify individual components + aBox.CornerMin().x() = -1.0; + aBox.CornerMin().y() = -2.0; + aBox.CornerMax().z() = 15.0; + + EXPECT_NEAR(aBox.CornerMin().x(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().y(), -2.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMin().z(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aBox.CornerMax().z(), 15.0, Precision::Confusion()); +} + +// ======================================================================================= +// Additional Edge Cases +// ======================================================================================= + +TEST(BVH_BoxTest, Area_DegenerateBox2D) +{ + // 2D box with zero area should return perimeter + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(5.0, 0.0)); + + Standard_Real anArea = aBox.Area(); + // For degenerate case, returns sum of dimensions + EXPECT_GE(anArea, 0.0); + EXPECT_NEAR(anArea, 5.0, Precision::Confusion()); // Width + height = 5 + 0 +} + +TEST(BVH_BoxTest, IsOut_TwoCorners_Overlapping) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Test with overlapping region + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(5.0, 5.0, 5.0), BVH_Vec3d(15.0, 15.0, 15.0))); +} + +TEST(BVH_BoxTest, IsOut_TwoCorners_Disjoint) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Test with disjoint region + EXPECT_TRUE(aBox.IsOut(BVH_Vec3d(20.0, 20.0, 20.0), BVH_Vec3d(30.0, 30.0, 30.0))); +} + +TEST(BVH_BoxTest, IsOut_TwoCorners_Contained) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + + // Test with fully contained region + EXPECT_FALSE(aBox.IsOut(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(8.0, 8.0, 8.0))); +} + +TEST(BVH_BoxTest, Constexpr_Construction) +{ + // Test that constexpr constructors work at compile time + constexpr BVH_Vec3d aMin(0.0, 0.0, 0.0); + constexpr BVH_Vec3d aMax(1.0, 1.0, 1.0); + constexpr BVH_Box aBox(aMin, aMax); + + static_assert(aBox.IsValid(), "Constexpr box should be valid"); +} + +TEST(BVH_BoxTest, Constexpr_SinglePoint) +{ + constexpr BVH_Vec3d aPoint(5.0, 5.0, 5.0); + constexpr BVH_Box aBox(aPoint); + + static_assert(aBox.IsValid(), "Constexpr single-point box should be valid"); +} + +TEST(BVH_BoxTest, Constexpr_DefaultInvalid) +{ + constexpr BVH_Box aBox; + + static_assert(!aBox.IsValid(), "Default constexpr box should be invalid"); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_BuildQueue_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_BuildQueue_Test.cxx new file mode 100644 index 0000000000..62d7225ce8 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_BuildQueue_Test.cxx @@ -0,0 +1,387 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include + +#include +#include + +// ============================================================================= +// BVH_BuildQueue Basic Tests +// ============================================================================= + +TEST(BVH_BuildQueueTest, DefaultConstructor) +{ + BVH_BuildQueue aQueue; + + EXPECT_EQ(aQueue.Size(), 0); + EXPECT_FALSE(aQueue.HasBusyThreads()); +} + +TEST(BVH_BuildQueueTest, EnqueueSingle) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(42); + + EXPECT_EQ(aQueue.Size(), 1); +} + +TEST(BVH_BuildQueueTest, EnqueueMultiple) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(1); + aQueue.Enqueue(2); + aQueue.Enqueue(3); + + EXPECT_EQ(aQueue.Size(), 3); +} + +TEST(BVH_BuildQueueTest, FetchFromEmptyQueue) +{ + BVH_BuildQueue aQueue; + Standard_Boolean wasBusy = Standard_False; + Standard_Integer aResult = aQueue.Fetch(wasBusy); + + EXPECT_EQ(aResult, -1); + EXPECT_FALSE(wasBusy); + EXPECT_FALSE(aQueue.HasBusyThreads()); +} + +TEST(BVH_BuildQueueTest, FetchSingleItem) +{ + BVH_BuildQueue aQueue; + aQueue.Enqueue(42); + + Standard_Boolean wasBusy = Standard_False; + Standard_Integer aResult = aQueue.Fetch(wasBusy); + + EXPECT_EQ(aResult, 42); + EXPECT_TRUE(wasBusy); + EXPECT_TRUE(aQueue.HasBusyThreads()); // Thread became busy + EXPECT_EQ(aQueue.Size(), 0); +} + +TEST(BVH_BuildQueueTest, FetchFIFOOrder) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(10); + aQueue.Enqueue(20); + aQueue.Enqueue(30); + + Standard_Boolean wasBusy = Standard_False; + + EXPECT_EQ(aQueue.Fetch(wasBusy), 10); + EXPECT_TRUE(wasBusy); + + EXPECT_EQ(aQueue.Fetch(wasBusy), 20); + EXPECT_TRUE(wasBusy); + + EXPECT_EQ(aQueue.Fetch(wasBusy), 30); + EXPECT_TRUE(wasBusy); + + EXPECT_EQ(aQueue.Fetch(wasBusy), -1); + EXPECT_FALSE(wasBusy); +} + +TEST(BVH_BuildQueueTest, ThreadCountTracking) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(1); + aQueue.Enqueue(2); + + EXPECT_FALSE(aQueue.HasBusyThreads()); + + Standard_Boolean wasBusy = Standard_False; + + // First fetch - thread becomes busy + aQueue.Fetch(wasBusy); + EXPECT_TRUE(wasBusy); + EXPECT_TRUE(aQueue.HasBusyThreads()); + + // Second fetch while already busy + aQueue.Fetch(wasBusy); + EXPECT_TRUE(wasBusy); + EXPECT_TRUE(aQueue.HasBusyThreads()); + + // Fetch from empty queue while busy + aQueue.Fetch(wasBusy); + EXPECT_FALSE(wasBusy); + EXPECT_FALSE(aQueue.HasBusyThreads()); +} + +TEST(BVH_BuildQueueTest, AlternatingEnqueueFetch) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(1); + EXPECT_EQ(aQueue.Size(), 1); + + Standard_Boolean wasBusy = Standard_False; + EXPECT_EQ(aQueue.Fetch(wasBusy), 1); + EXPECT_EQ(aQueue.Size(), 0); + + aQueue.Enqueue(2); + EXPECT_EQ(aQueue.Size(), 1); + + EXPECT_EQ(aQueue.Fetch(wasBusy), 2); + EXPECT_EQ(aQueue.Size(), 0); +} + +TEST(BVH_BuildQueueTest, LargeQueue) +{ + BVH_BuildQueue aQueue; + + const int aCount = 1000; + for (int i = 0; i < aCount; ++i) + { + aQueue.Enqueue(i); + } + + EXPECT_EQ(aQueue.Size(), aCount); + + Standard_Boolean wasBusy = Standard_False; + for (int i = 0; i < aCount; ++i) + { + EXPECT_EQ(aQueue.Fetch(wasBusy), i); + } + + EXPECT_EQ(aQueue.Size(), 0); +} + +TEST(BVH_BuildQueueTest, NegativeValues) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(-1); + aQueue.Enqueue(-100); + aQueue.Enqueue(0); + + Standard_Boolean wasBusy = Standard_False; + + EXPECT_EQ(aQueue.Fetch(wasBusy), -1); + EXPECT_EQ(aQueue.Fetch(wasBusy), -100); + EXPECT_EQ(aQueue.Fetch(wasBusy), 0); +} + +// ============================================================================= +// BVH_BuildQueue Thread Safety Tests +// ============================================================================= + +TEST(BVH_BuildQueueTest, ConcurrentEnqueue) +{ + BVH_BuildQueue aQueue; + + const int aThreadCount = 4; + const int aItemsPerThread = 100; + std::vector aThreads; + + for (int t = 0; t < aThreadCount; ++t) + { + aThreads.emplace_back([&aQueue, t, aItemsPerThread = aItemsPerThread]() { + for (int i = 0; i < aItemsPerThread; ++i) + { + aQueue.Enqueue(t * aItemsPerThread + i); + } + }); + } + + for (auto& aThread : aThreads) + { + aThread.join(); + } + + EXPECT_EQ(aQueue.Size(), aThreadCount * aItemsPerThread); +} + +TEST(BVH_BuildQueueTest, ConcurrentFetch) +{ + BVH_BuildQueue aQueue; + + const int aItemCount = 400; + for (int i = 0; i < aItemCount; ++i) + { + aQueue.Enqueue(i); + } + + const int aThreadCount = 4; + std::vector aThreads; + std::vector aFetchedCounts(aThreadCount, 0); + + for (int t = 0; t < aThreadCount; ++t) + { + aThreads.emplace_back([&aQueue, t, &aFetchedCounts]() { + Standard_Boolean wasBusy = Standard_False; + while (true) + { + Standard_Integer aItem = aQueue.Fetch(wasBusy); + if (aItem == -1) + break; + aFetchedCounts[t]++; + } + }); + } + + for (auto& aThread : aThreads) + { + aThread.join(); + } + + int aTotalFetched = 0; + for (int aCount : aFetchedCounts) + { + aTotalFetched += aCount; + } + + EXPECT_EQ(aTotalFetched, aItemCount); + EXPECT_EQ(aQueue.Size(), 0); +} + +TEST(BVH_BuildQueueTest, ConcurrentEnqueueAndFetch) +{ + BVH_BuildQueue aQueue; + + const int aProducerCount = 2; + const int aConsumerCount = 2; + const int aItemsPerProducer = 100; + std::vector aThreads; + std::atomic aFetchedCount{0}; + std::atomic aDone{false}; + + // Producer threads + for (int t = 0; t < aProducerCount; ++t) + { + aThreads.emplace_back([&aQueue, t, aItemsPerProducer = aItemsPerProducer]() { + for (int i = 0; i < aItemsPerProducer; ++i) + { + aQueue.Enqueue(t * aItemsPerProducer + i); + } + }); + } + + // Consumer threads + for (int t = 0; t < aConsumerCount; ++t) + { + aThreads.emplace_back([&aQueue, &aFetchedCount, &aDone]() { + Standard_Boolean wasBusy = Standard_False; + while (!aDone.load() || aQueue.Size() > 0) + { + Standard_Integer aItem = aQueue.Fetch(wasBusy); + if (aItem != -1) + { + aFetchedCount.fetch_add(1); + } + } + }); + } + + // Wait for producers to finish + for (int i = 0; i < aProducerCount; ++i) + { + aThreads[i].join(); + } + + // Signal consumers that production is done + aDone.store(true); + + // Wait for consumers to finish + for (int i = aProducerCount; i < aProducerCount + aConsumerCount; ++i) + { + aThreads[i].join(); + } + + EXPECT_EQ(aFetchedCount.load(), aProducerCount * aItemsPerProducer); + EXPECT_EQ(aQueue.Size(), 0); +} + +// ============================================================================= +// BVH_BuildQueue Edge Cases +// ============================================================================= + +TEST(BVH_BuildQueueTest, RepeatedFetchFromEmpty) +{ + BVH_BuildQueue aQueue; + Standard_Boolean wasBusy = Standard_False; + + for (int i = 0; i < 10; ++i) + { + Standard_Integer aResult = aQueue.Fetch(wasBusy); + EXPECT_EQ(aResult, -1); + EXPECT_FALSE(wasBusy); + } + + EXPECT_FALSE(aQueue.HasBusyThreads()); +} + +TEST(BVH_BuildQueueTest, EnqueueZero) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(0); + EXPECT_EQ(aQueue.Size(), 1); + + Standard_Boolean wasBusy = Standard_False; + EXPECT_EQ(aQueue.Fetch(wasBusy), 0); + EXPECT_TRUE(wasBusy); +} + +TEST(BVH_BuildQueueTest, DuplicateValues) +{ + BVH_BuildQueue aQueue; + + aQueue.Enqueue(5); + aQueue.Enqueue(5); + aQueue.Enqueue(5); + + EXPECT_EQ(aQueue.Size(), 3); + + Standard_Boolean wasBusy = Standard_False; + EXPECT_EQ(aQueue.Fetch(wasBusy), 5); + EXPECT_EQ(aQueue.Fetch(wasBusy), 5); + EXPECT_EQ(aQueue.Fetch(wasBusy), 5); +} + +TEST(BVH_BuildQueueTest, SingleThreadWorkflow) +{ + BVH_BuildQueue aQueue; + + // Simulate single-threaded BVH building workflow + aQueue.Enqueue(0); // Root node + + Standard_Boolean wasBusy = Standard_False; + Standard_Integer aNode = aQueue.Fetch(wasBusy); + + EXPECT_EQ(aNode, 0); + EXPECT_TRUE(wasBusy); + + // After processing root, enqueue children + aQueue.Enqueue(1); // Left child + aQueue.Enqueue(2); // Right child + + EXPECT_EQ(aQueue.Size(), 2); + + aNode = aQueue.Fetch(wasBusy); + EXPECT_EQ(aNode, 1); + + aNode = aQueue.Fetch(wasBusy); + EXPECT_EQ(aNode, 2); + + aNode = aQueue.Fetch(wasBusy); + EXPECT_EQ(aNode, -1); + EXPECT_FALSE(wasBusy); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_LinearBuilder_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_LinearBuilder_Test.cxx new file mode 100644 index 0000000000..077f445bea --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_LinearBuilder_Test.cxx @@ -0,0 +1,337 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include +#include + +TEST(BVH_LinearBuilderTest, DefaultConstructor) +{ + BVH_LinearBuilder aBuilder; + + EXPECT_GT(aBuilder.LeafNodeSize(), 0); + EXPECT_GT(aBuilder.MaxTreeDepth(), 0); +} + +TEST(BVH_LinearBuilderTest, CustomParameters) +{ + BVH_LinearBuilder aBuilder(5, 20); + + EXPECT_EQ(aBuilder.LeafNodeSize(), 5); + EXPECT_EQ(aBuilder.MaxTreeDepth(), 20); +} + +TEST(BVH_LinearBuilderTest, BuildEmptySet) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(); + BVH_BoxSet aBoxSet(aBuilder); + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_EQ(aBVH->Length(), 0); +} + +TEST(BVH_LinearBuilderTest, BuildSingleElement) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(); + BVH_BoxSet aBoxSet(aBuilder); + + aBoxSet.Add(0, BVH_Box(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0))); + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_EQ(aBVH->Length(), 1); + EXPECT_TRUE(aBVH->IsOuter(0)); +} + +TEST(BVH_LinearBuilderTest, BuildMultipleElements) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes along X axis + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + EXPECT_GT(aBVH->Depth(), 0); +} + +TEST(BVH_LinearBuilderTest, MortonCodeSorting) +{ + // Linear builder uses Morton codes for spatial sorting + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes in a pattern that tests Morton code sorting + // Diagonal pattern should group nearby boxes + for (int i = 0; i < 8; ++i) + { + Standard_Real x = (i & 1) ? 10.0 : 0.0; + Standard_Real y = (i & 2) ? 10.0 : 0.0; + Standard_Real z = (i & 4) ? 10.0 : 0.0; + + BVH_Box aBox(BVH_Vec3d(x, y, z), BVH_Vec3d(x + 1.0, y + 1.0, z + 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + + // SAH should be reasonable for spatially sorted data + Standard_Real aSAH = aBVH->EstimateSAH(); + EXPECT_GT(aSAH, 0.0); +} + +TEST(BVH_LinearBuilderTest, LeafNodeSizeRespected) +{ + const int aLeafSize = 3; + opencascade::handle> aBuilder = + new BVH_LinearBuilder(aLeafSize, 32); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + + // Check that leaf nodes don't exceed leaf size + for (int i = 0; i < aBVH->Length(); ++i) + { + if (aBVH->IsOuter(i)) + { + int aNbPrims = aBVH->NbPrimitives(i); + EXPECT_LE(aNbPrims, aLeafSize); + } + } +} + +TEST(BVH_LinearBuilderTest, Build2D) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec2d(i * 2.0, 0.0), BVH_Vec2d(i * 2.0 + 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} + +// Note: Float tests skipped due to BVH_BoxSet::Center return type issue + +TEST(BVH_LinearBuilderTest, LargeDataSet) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(4, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add many boxes - this tests the Morton code optimization + int aCount = 0; + for (int x = 0; x < 10; ++x) + { + for (int y = 0; y < 10; ++y) + { + for (int z = 0; z < 10; ++z) + { + BVH_Box aBox(BVH_Vec3d(x * 2.0, y * 2.0, z * 2.0), + BVH_Vec3d(x * 2.0 + 1.0, y * 2.0 + 1.0, z * 2.0 + 1.0)); + aBoxSet.Add(aCount++, aBox); + } + } + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + + // Verify tree covers all primitives + int aTotalPrims = 0; + for (int i = 0; i < aBVH->Length(); ++i) + { + if (aBVH->IsOuter(i)) + { + aTotalPrims += aBVH->NbPrimitives(i); + } + } + EXPECT_EQ(aTotalPrims, 1000); +} + +TEST(BVH_LinearBuilderTest, MaxDepthParameter) +{ + // Linear builder uses max depth parameter but may exceed it in some cases + const int aMaxDepth = 10; + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, aMaxDepth); + BVH_BoxSet aBoxSet(aBuilder); + + for (int i = 0; i < 100; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + // Tree should have a reasonable depth + EXPECT_GT(aBVH->Depth(), 0); + EXPECT_LT(aBVH->Depth(), 20); +} + +TEST(BVH_LinearBuilderTest, ClusteredData) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Create two clusters far apart + // Morton codes should group each cluster together + for (int i = 0; i < 10; ++i) + { + // Cluster 1 near origin + BVH_Box aBox1(BVH_Vec3d(i * 0.1, i * 0.1, i * 0.1), + BVH_Vec3d(i * 0.1 + 0.1, i * 0.1 + 0.1, i * 0.1 + 0.1)); + aBoxSet.Add(i, aBox1); + + // Cluster 2 far from origin + BVH_Box aBox2(BVH_Vec3d(100.0 + i * 0.1, 100.0 + i * 0.1, 100.0 + i * 0.1), + BVH_Vec3d(100.1 + i * 0.1, 100.1 + i * 0.1, 100.1 + i * 0.1)); + aBoxSet.Add(i + 10, aBox2); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); + + // SAH should be reasonable for clustered data + Standard_Real aSAH = aBVH->EstimateSAH(); + EXPECT_GT(aSAH, 0.0); +} + +TEST(BVH_LinearBuilderTest, OverlappingBoxes) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add overlapping boxes + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 0.5, 0.0, 0.0), + BVH_Vec3d(i * 0.5 + 2.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} + +TEST(BVH_LinearBuilderTest, IdenticalBoxes) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add identical boxes (same Morton code) + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GE(aBVH->Length(), 1); +} + +TEST(BVH_LinearBuilderTest, CompareWithBinnedBuilder) +{ + // Linear builder should produce reasonable trees compared to binned builder + opencascade::handle> aLinearBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aLinearSet(aLinearBuilder); + + for (int i = 0; i < 50; ++i) + { + BVH_Box aBox(BVH_Vec3d(i * 2.0, 0.0, 0.0), + BVH_Vec3d(i * 2.0 + 1.0, 1.0, 1.0)); + aLinearSet.Add(i, aBox); + } + + aLinearSet.Build(); + + const opencascade::handle>& aLinearBVH = aLinearSet.BVH(); + + // Linear builder produces a valid tree + EXPECT_GT(aLinearBVH->Length(), 1); + EXPECT_GT(aLinearBVH->Depth(), 0); + + // SAH should be positive + Standard_Real aSAH = aLinearBVH->EstimateSAH(); + EXPECT_GT(aSAH, 0.0); +} + +TEST(BVH_LinearBuilderTest, NegativeCoordinates) +{ + opencascade::handle> aBuilder = + new BVH_LinearBuilder(1, 32); + BVH_BoxSet aBoxSet(aBuilder); + + // Add boxes with negative coordinates + for (int i = 0; i < 10; ++i) + { + BVH_Box aBox(BVH_Vec3d(-10.0 + i * 2.0, -5.0, -5.0), + BVH_Vec3d(-9.0 + i * 2.0, -4.0, -4.0)); + aBoxSet.Add(i, aBox); + } + + aBoxSet.Build(); + + const opencascade::handle>& aBVH = aBoxSet.BVH(); + EXPECT_GT(aBVH->Length(), 1); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_QuickSorter_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_QuickSorter_Test.cxx new file mode 100644 index 0000000000..dadf105968 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_QuickSorter_Test.cxx @@ -0,0 +1,330 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include + +// ============================================================================= +// BVH_QuickSorter Basic Tests +// ============================================================================= + +TEST(BVH_QuickSorterTest, Constructor) +{ + BVH_QuickSorter aSorterX(0); + BVH_QuickSorter aSorterY(1); + BVH_QuickSorter aSorterZ(2); + + // Constructor should not crash + EXPECT_TRUE(true); +} + +TEST(BVH_QuickSorterTest, SortEmptySet) +{ + BVH_Triangulation aTriangulation; + BVH_QuickSorter aSorter(0); + + // Sorting empty set should not crash + aSorter.Perform(&aTriangulation); + EXPECT_EQ(aTriangulation.Size(), 0); +} + +TEST(BVH_QuickSorterTest, SortSingleElement) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // Single element should remain unchanged + EXPECT_EQ(aTriangulation.Size(), 1); +} + +// ============================================================================= +// Sorting Correctness Tests +// ============================================================================= + +TEST(BVH_QuickSorterTest, SortAlongXAxis) +{ + BVH_Triangulation aTriangulation; + + // Create triangles with increasing X centroids: 5, 1, 3 + // Triangle 0: centroid X = 5 + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(4.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(5.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(6.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + // Triangle 1: centroid X = 1 + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(3, 4, 5, 0)); + + // Triangle 2: centroid X = 3 + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(3.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(4.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(6, 7, 8, 0)); + + BVH_QuickSorter aSorter(0); // Sort along X + aSorter.Perform(&aTriangulation); + + // After sorting: should be ordered 1, 3, 5 (indices 1, 2, 0) + EXPECT_LT(aTriangulation.Center(0, 0), aTriangulation.Center(1, 0)); + EXPECT_LT(aTriangulation.Center(1, 0), aTriangulation.Center(2, 0)); + + // Verify actual values + EXPECT_NEAR(aTriangulation.Center(0, 0), 1.0, 1e-10); + EXPECT_NEAR(aTriangulation.Center(1, 0), 3.0, 1e-10); + EXPECT_NEAR(aTriangulation.Center(2, 0), 5.0, 1e-10); +} + +TEST(BVH_QuickSorterTest, SortAlongYAxis) +{ + BVH_Triangulation aTriangulation; + + // Create triangles with Y centroids: 3, 1, 2 + for (int i = 0; i < 3; ++i) + { + Standard_Real y = (i == 0 ? 3.0 : (i == 1 ? 1.0 : 2.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, y - 0.5, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, y, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.5, y + 0.5, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(1); // Sort along Y + aSorter.Perform(&aTriangulation); + + // Should be ordered by Y centroid + EXPECT_LT(aTriangulation.Center(0, 1), aTriangulation.Center(1, 1)); + EXPECT_LT(aTriangulation.Center(1, 1), aTriangulation.Center(2, 1)); +} + +TEST(BVH_QuickSorterTest, SortAlongZAxis) +{ + BVH_Triangulation aTriangulation; + + // Create triangles with Z centroids: 2, 0, 1 + for (int i = 0; i < 3; ++i) + { + Standard_Real z = (i == 0 ? 2.0 : (i == 1 ? 0.0 : 1.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, z)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, z)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.5, 1.0, z)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(2); // Sort along Z + aSorter.Perform(&aTriangulation); + + // Should be ordered by Z centroid + EXPECT_LT(aTriangulation.Center(0, 2), aTriangulation.Center(1, 2)); + EXPECT_LT(aTriangulation.Center(1, 2), aTriangulation.Center(2, 2)); +} + +TEST(BVH_QuickSorterTest, SortRangeInSet) +{ + BVH_Triangulation aTriangulation; + + // Create 5 triangles with X centroids: 0, 1, 2, 3, 4 + for (int i = 0; i < 5; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + // Reverse the middle 3 elements (indices 1, 2, 3) manually + aTriangulation.Swap(1, 3); + + // Now we have centroids: 0, 3, 2, 1, 4 + EXPECT_NEAR(aTriangulation.Center(1, 0), 3.5, 1e-10); + EXPECT_NEAR(aTriangulation.Center(2, 0), 2.5, 1e-10); + EXPECT_NEAR(aTriangulation.Center(3, 0), 1.5, 1e-10); + + // Sort only range [1, 3] (middle 3 elements) + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation, 1, 3); + + // Elements 0 and 4 should remain unchanged + EXPECT_NEAR(aTriangulation.Center(0, 0), 0.5, 1e-10); + EXPECT_NEAR(aTriangulation.Center(4, 0), 4.5, 1e-10); + + // Elements 1-3 should be sorted: 1, 2, 3 + EXPECT_NEAR(aTriangulation.Center(1, 0), 1.5, 1e-10); + EXPECT_NEAR(aTriangulation.Center(2, 0), 2.5, 1e-10); + EXPECT_NEAR(aTriangulation.Center(3, 0), 3.5, 1e-10); +} + +// ============================================================================= +// Edge Cases and Stability Tests +// ============================================================================= + +TEST(BVH_QuickSorterTest, AlreadySorted) +{ + BVH_Triangulation aTriangulation; + + // Create already sorted triangles + for (int i = 0; i < 10; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // Should remain sorted + for (int i = 0; i < 9; ++i) + { + EXPECT_LT(aTriangulation.Center(i, 0), aTriangulation.Center(i + 1, 0)); + } +} + +TEST(BVH_QuickSorterTest, ReverseSorted) +{ + BVH_Triangulation aTriangulation; + + // Create reverse sorted triangles + for (int i = 0; i < 10; ++i) + { + Standard_Real x = static_cast(9 - i); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // Should become sorted + for (int i = 0; i < 9; ++i) + { + EXPECT_LT(aTriangulation.Center(i, 0), aTriangulation.Center(i + 1, 0)); + } +} + +TEST(BVH_QuickSorterTest, DuplicateValues) +{ + BVH_Triangulation aTriangulation; + + // Create triangles with duplicate centroids: 1, 2, 2, 2, 3 + for (int i = 0; i < 5; ++i) + { + Standard_Real x = (i == 0 ? 1.0 : (i <= 3 ? 2.0 : 3.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // Should be non-decreasing + for (int i = 0; i < 4; ++i) + { + EXPECT_LE(aTriangulation.Center(i, 0), aTriangulation.Center(i + 1, 0)); + } +} + +TEST(BVH_QuickSorterTest, AllSameValue) +{ + BVH_Triangulation aTriangulation; + + // All triangles at same position + for (int i = 0; i < 5; ++i) + { + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // All should have same centroid + for (int i = 0; i < 5; ++i) + { + EXPECT_NEAR(aTriangulation.Center(i, 0), 1.467, 1e-2); + } +} + +TEST(BVH_QuickSorterTest, LargeDataSet) +{ + BVH_Triangulation aTriangulation; + + // Create 1000 triangles with random-ish order + for (int i = 0; i < 1000; ++i) + { + // Use simple pseudo-random pattern + Standard_Real x = static_cast((i * 37) % 1000); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + // Verify sorted + for (int i = 0; i < 999; ++i) + { + EXPECT_LE(aTriangulation.Center(i, 0), aTriangulation.Center(i + 1, 0)); + } +} + +TEST(BVH_QuickSorterTest, TwoElements) +{ + BVH_Triangulation aTriangulation; + + // Two elements out of order + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(3.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(3, 4, 5, 0)); + + BVH_QuickSorter aSorter(0); + aSorter.Perform(&aTriangulation); + + EXPECT_LT(aTriangulation.Center(0, 0), aTriangulation.Center(1, 0)); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_RadixSorter_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_RadixSorter_Test.cxx new file mode 100644 index 0000000000..3ca48fa71e --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_RadixSorter_Test.cxx @@ -0,0 +1,449 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include + +// ============================================================================= +// Morton Code Tests +// ============================================================================= + +TEST(BVH_RadixSorterTest, EncodeMortonCode_Origin) +{ + unsigned int aCode = BVH::EncodeMortonCode(0, 0, 0); + EXPECT_EQ(aCode, 0u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SingleBits) +{ + // X=1, Y=0, Z=0 should give 1 (bit 0) + unsigned int aCodeX = BVH::EncodeMortonCode(1, 0, 0); + EXPECT_EQ(aCodeX, 1u); + + // X=0, Y=1, Z=0 should give 2 (bit 1) + unsigned int aCodeY = BVH::EncodeMortonCode(0, 1, 0); + EXPECT_EQ(aCodeY, 2u); + + // X=0, Y=0, Z=1 should give 4 (bit 2) + unsigned int aCodeZ = BVH::EncodeMortonCode(0, 0, 1); + EXPECT_EQ(aCodeZ, 4u); + + // X=1, Y=1, Z=1 should give 7 (bits 0, 1, 2) + unsigned int aCodeXYZ = BVH::EncodeMortonCode(1, 1, 1); + EXPECT_EQ(aCodeXYZ, 7u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_Interleaving) +{ + // X=2 (binary 10), Y=0, Z=0 + // Should produce binary ...001000 = 8 + unsigned int aCode1 = BVH::EncodeMortonCode(2, 0, 0); + EXPECT_EQ(aCode1, 8u); + + // X=0, Y=2, Z=0 + // Should produce binary ...010000 = 16 + unsigned int aCode2 = BVH::EncodeMortonCode(0, 2, 0); + EXPECT_EQ(aCode2, 16u); + + // X=0, Y=0, Z=2 + // Should produce binary ...100000 = 32 + unsigned int aCode3 = BVH::EncodeMortonCode(0, 0, 2); + EXPECT_EQ(aCode3, 32u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_MaxValues) +{ + // Test with maximum 10-bit values (1023) + unsigned int aCode = BVH::EncodeMortonCode(1023, 1023, 1023); + + // All 30 bits should be set + EXPECT_EQ(aCode, 0x3FFFFFFFu); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_Symmetry) +{ + // Swapping coordinates should produce predictable results + unsigned int aCode1 = BVH::EncodeMortonCode(100, 200, 300); + unsigned int aCode2 = BVH::EncodeMortonCode(200, 100, 300); + unsigned int aCode3 = BVH::EncodeMortonCode(100, 300, 200); + + // Codes should be different + EXPECT_NE(aCode1, aCode2); + EXPECT_NE(aCode1, aCode3); + EXPECT_NE(aCode2, aCode3); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_Ordering) +{ + // Points closer in space should have closer Morton codes + // (0, 0, 0) vs (1, 0, 0) should be closer than (0, 0, 0) vs (0, 0, 1023) + unsigned int aCode000 = BVH::EncodeMortonCode(0, 0, 0); + unsigned int aCode100 = BVH::EncodeMortonCode(1, 0, 0); + unsigned int aCode001023 = BVH::EncodeMortonCode(0, 0, 1023); + + // (0,0,0) to (1,0,0) difference should be small + unsigned int aDiff1 = aCode100 - aCode000; + + // (0,0,0) to (0,0,1023) difference should be large + unsigned int aDiff2 = aCode001023 - aCode000; + + EXPECT_LT(aDiff1, aDiff2); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_MidValues) +{ + // Test with mid-range values + unsigned int aCode = BVH::EncodeMortonCode(512, 512, 512); + + // Should be non-zero and within 30-bit range + EXPECT_GT(aCode, 0u); + EXPECT_LE(aCode, 0x3FFFFFFFu); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_PowersOfTwo) +{ + // Test powers of two for each axis + unsigned int aCode4 = BVH::EncodeMortonCode(4, 0, 0); // X=100 binary + unsigned int aCode8 = BVH::EncodeMortonCode(8, 0, 0); // X=1000 binary + unsigned int aCode16 = BVH::EncodeMortonCode(16, 0, 0); // X=10000 binary + + // Each should be 8x the previous (3 bit positions apart in Morton code) + EXPECT_EQ(aCode8, aCode4 * 8); + EXPECT_EQ(aCode16, aCode8 * 8); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_ConsistentEncoding) +{ + // Same inputs should always produce same outputs + for (int i = 0; i < 100; ++i) + { + unsigned int aCode1 = BVH::EncodeMortonCode(100, 200, 300); + unsigned int aCode2 = BVH::EncodeMortonCode(100, 200, 300); + EXPECT_EQ(aCode1, aCode2); + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_BoundaryValues) +{ + // Test boundary values near max (1023) + unsigned int aCode1022 = BVH::EncodeMortonCode(1022, 1022, 1022); + unsigned int aCode1023 = BVH::EncodeMortonCode(1023, 1023, 1023); + + EXPECT_LT(aCode1022, aCode1023); + EXPECT_EQ(aCode1023, 0x3FFFFFFFu); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SmallValues) +{ + // Test small values + unsigned int aCode2 = BVH::EncodeMortonCode(2, 2, 2); + unsigned int aCode3 = BVH::EncodeMortonCode(3, 3, 3); + + // Both should be small but aCode3 > aCode2 + EXPECT_LT(aCode2, 100u); + EXPECT_LT(aCode3, 100u); + EXPECT_LT(aCode2, aCode3); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SingleAxisValues) +{ + // Test with value only in one axis + unsigned int aCodeX = BVH::EncodeMortonCode(255, 0, 0); + unsigned int aCodeY = BVH::EncodeMortonCode(0, 255, 0); + unsigned int aCodeZ = BVH::EncodeMortonCode(0, 0, 255); + + // X bits in positions 0, 3, 6, ... + // Y bits in positions 1, 4, 7, ... + // Z bits in positions 2, 5, 8, ... + EXPECT_NE(aCodeX, aCodeY); + EXPECT_NE(aCodeY, aCodeZ); + EXPECT_NE(aCodeX, aCodeZ); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SequentialXValues) +{ + // Test that sequential X values produce increasing codes + unsigned int aPrev = BVH::EncodeMortonCode(0, 500, 500); + for (unsigned int x = 1; x <= 10; ++x) + { + unsigned int aCurr = BVH::EncodeMortonCode(x, 500, 500); + EXPECT_GT(aCurr, aPrev); + aPrev = aCurr; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SequentialYValues) +{ + // Test that sequential Y values produce increasing codes + unsigned int aPrev = BVH::EncodeMortonCode(500, 0, 500); + for (unsigned int y = 1; y <= 10; ++y) + { + unsigned int aCurr = BVH::EncodeMortonCode(500, y, 500); + EXPECT_GT(aCurr, aPrev); + aPrev = aCurr; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SequentialZValues) +{ + // Test that sequential Z values produce increasing codes + unsigned int aPrev = BVH::EncodeMortonCode(500, 500, 0); + for (unsigned int z = 1; z <= 10; ++z) + { + unsigned int aCurr = BVH::EncodeMortonCode(500, 500, z); + EXPECT_GT(aCurr, aPrev); + aPrev = aCurr; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_ZeroInDimensions) +{ + // Test with zeros in different dimensions + unsigned int aCodeXY = BVH::EncodeMortonCode(100, 100, 0); + unsigned int aCodeXZ = BVH::EncodeMortonCode(100, 0, 100); + unsigned int aCodeYZ = BVH::EncodeMortonCode(0, 100, 100); + + // All should be different + EXPECT_NE(aCodeXY, aCodeXZ); + EXPECT_NE(aCodeXY, aCodeYZ); + EXPECT_NE(aCodeXZ, aCodeYZ); + + // All should be non-zero + EXPECT_GT(aCodeXY, 0u); + EXPECT_GT(aCodeXZ, 0u); + EXPECT_GT(aCodeYZ, 0u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_UpperByteBits) +{ + // Test values that require upper byte (values >= 256) + unsigned int aCode256 = BVH::EncodeMortonCode(256, 0, 0); + unsigned int aCode512 = BVH::EncodeMortonCode(512, 0, 0); + unsigned int aCode768 = BVH::EncodeMortonCode(768, 0, 0); + + EXPECT_GT(aCode256, 0u); + EXPECT_GT(aCode512, aCode256); + EXPECT_GT(aCode768, aCode512); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_AllOnes) +{ + // 255 = 0xFF (all ones in lower byte) + unsigned int aCode = BVH::EncodeMortonCode(255, 255, 255); + + // Should be less than max (1023, 1023, 1023) + EXPECT_LT(aCode, 0x3FFFFFFFu); + EXPECT_GT(aCode, 0u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_LocalityPreservation) +{ + // Points close in space should have relatively close Morton codes + unsigned int aCenter = BVH::EncodeMortonCode(512, 512, 512); + unsigned int aNear1 = BVH::EncodeMortonCode(513, 512, 512); + unsigned int aNear2 = BVH::EncodeMortonCode(512, 513, 512); + unsigned int aFar = BVH::EncodeMortonCode(0, 0, 0); + + // Near points should have smaller difference than far point + unsigned int aDiffNear1 = (aNear1 > aCenter) ? (aNear1 - aCenter) : (aCenter - aNear1); + unsigned int aDiffNear2 = (aNear2 > aCenter) ? (aNear2 - aCenter) : (aCenter - aNear2); + unsigned int aDiffFar = (aFar > aCenter) ? (aFar - aCenter) : (aCenter - aFar); + + EXPECT_LT(aDiffNear1, aDiffFar); + EXPECT_LT(aDiffNear2, aDiffFar); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_BitPattern3) +{ + // X=3 (binary 11), Y=0, Z=0 + // Interleaved: ...001001 = 9 + unsigned int aCode = BVH::EncodeMortonCode(3, 0, 0); + EXPECT_EQ(aCode, 9u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_BitPattern7) +{ + // X=7 (binary 111), Y=0, Z=0 + // Interleaved: 001001001 = 73 + unsigned int aCode = BVH::EncodeMortonCode(7, 0, 0); + EXPECT_EQ(aCode, 73u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_XYEqual) +{ + // When X = Y and Z = 0, code should follow pattern + unsigned int aCode1 = BVH::EncodeMortonCode(1, 1, 0); + unsigned int aCode2 = BVH::EncodeMortonCode(2, 2, 0); + unsigned int aCode3 = BVH::EncodeMortonCode(4, 4, 0); + + // X=1, Y=1, Z=0: bits at 0,1 -> 3 + EXPECT_EQ(aCode1, 3u); + + // X=2, Y=2, Z=0: bits at 3,4 -> 24 + EXPECT_EQ(aCode2, 24u); + + // X=4, Y=4, Z=0: bits at 6,7 -> 192 + EXPECT_EQ(aCode3, 192u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_AllAxesEqual) +{ + // When all axes are equal + unsigned int aCode1 = BVH::EncodeMortonCode(1, 1, 1); + unsigned int aCode2 = BVH::EncodeMortonCode(2, 2, 2); + + // X=1, Y=1, Z=1: bits at 0,1,2 -> 7 + EXPECT_EQ(aCode1, 7u); + + // X=2, Y=2, Z=2: bits at 3,4,5 -> 56 + EXPECT_EQ(aCode2, 56u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_HalfMax) +{ + // Test with half of maximum (512) + unsigned int aCode = BVH::EncodeMortonCode(512, 512, 512); + + // Should be in upper half of range + EXPECT_GT(aCode, 0x1FFFFFFFu); + EXPECT_LE(aCode, 0x3FFFFFFFu); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_QuarterMax) +{ + // Test with quarter of maximum (256) + unsigned int aCode = BVH::EncodeMortonCode(256, 256, 256); + + // Should be less than half max + EXPECT_GT(aCode, 0u); + EXPECT_LT(aCode, 0x3FFFFFFFu); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_DifferentScales) +{ + // Test that scaling all coordinates by 2 shifts the Morton code + unsigned int aCode1 = BVH::EncodeMortonCode(100, 100, 100); + unsigned int aCode2 = BVH::EncodeMortonCode(200, 200, 200); + + // Code2 should be larger + EXPECT_GT(aCode2, aCode1); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_AdjacentCells) +{ + // Test adjacent cells in X direction + unsigned int aCode1 = BVH::EncodeMortonCode(100, 100, 100); + unsigned int aCode2 = BVH::EncodeMortonCode(101, 100, 100); + + // Difference should be 1 (X bit is at position 0) + EXPECT_EQ(aCode2 - aCode1, 1u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_AdjacentCellsY) +{ + // Test adjacent cells in Y direction + unsigned int aCode1 = BVH::EncodeMortonCode(100, 100, 100); + unsigned int aCode2 = BVH::EncodeMortonCode(100, 101, 100); + + // Difference should be 2 (Y bit is at position 1) + EXPECT_EQ(aCode2 - aCode1, 2u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_AdjacentCellsZ) +{ + // Test adjacent cells in Z direction + unsigned int aCode1 = BVH::EncodeMortonCode(100, 100, 100); + unsigned int aCode2 = BVH::EncodeMortonCode(100, 100, 101); + + // Difference should be 4 (Z bit is at position 2) + EXPECT_EQ(aCode2 - aCode1, 4u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_MonotonicX) +{ + // Verify monotonicity when only X changes + unsigned int aPrev = 0; + for (unsigned int x = 0; x < 100; ++x) + { + unsigned int aCode = BVH::EncodeMortonCode(x, 0, 0); + EXPECT_GE(aCode, aPrev); + aPrev = aCode; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_MonotonicY) +{ + // Verify monotonicity when only Y changes + unsigned int aPrev = 0; + for (unsigned int y = 0; y < 100; ++y) + { + unsigned int aCode = BVH::EncodeMortonCode(0, y, 0); + EXPECT_GE(aCode, aPrev); + aPrev = aCode; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_MonotonicZ) +{ + // Verify monotonicity when only Z changes + unsigned int aPrev = 0; + for (unsigned int z = 0; z < 100; ++z) + { + unsigned int aCode = BVH::EncodeMortonCode(0, 0, z); + EXPECT_GE(aCode, aPrev); + aPrev = aCode; + } +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_NoOverflow) +{ + // Verify no overflow with maximum values + unsigned int aCode = BVH::EncodeMortonCode(1023, 1023, 1023); + + // Should fit in 30 bits + EXPECT_EQ(aCode & 0xC0000000u, 0u); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_UniqueForDifferentInputs) +{ + // Different inputs should produce different codes + unsigned int aCode1 = BVH::EncodeMortonCode(100, 200, 300); + unsigned int aCode2 = BVH::EncodeMortonCode(100, 200, 301); + unsigned int aCode3 = BVH::EncodeMortonCode(100, 201, 300); + unsigned int aCode4 = BVH::EncodeMortonCode(101, 200, 300); + + EXPECT_NE(aCode1, aCode2); + EXPECT_NE(aCode1, aCode3); + EXPECT_NE(aCode1, aCode4); + EXPECT_NE(aCode2, aCode3); + EXPECT_NE(aCode2, aCode4); + EXPECT_NE(aCode3, aCode4); +} + +TEST(BVH_RadixSorterTest, EncodeMortonCode_SpecificPattern) +{ + // Test specific known pattern + // X=5 (101), Y=3 (011), Z=6 (110) + unsigned int aCode = BVH::EncodeMortonCode(5, 3, 6); + + // Morton code interleaves bits: X at 3i, Y at 3i+1, Z at 3i+2 + // The exact value depends on the interleaving order + // Just verify it's non-zero and within range + EXPECT_GT(aCode, 0u); + EXPECT_LE(aCode, 0x3FFFFFFFu); + + // Also verify consistency + unsigned int aCode2 = BVH::EncodeMortonCode(5, 3, 6); + EXPECT_EQ(aCode, aCode2); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Ray_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Ray_Test.cxx new file mode 100644 index 0000000000..c6728a455f --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Ray_Test.cxx @@ -0,0 +1,268 @@ +// Created on: 2025-01-20 +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include + +// ======================================================================================= +// Tests for BVH_Ray +// ======================================================================================= + +TEST(BVH_RayTest, ConstructorBasic) +{ + BVH_Vec3d aOrigin(1.0, 2.0, 3.0); + BVH_Vec3d aDirection(0.0, 1.0, 0.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Origin.x(), 1.0); + EXPECT_EQ(aRay.Origin.y(), 2.0); + EXPECT_EQ(aRay.Origin.z(), 3.0); + + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 1.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, DirectionStorage) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(2.0, 4.0, 8.0); + + BVH_Ray aRay(aOrigin, aDirection); + + // Verify direction is stored correctly + EXPECT_EQ(aRay.Direct.x(), 2.0); + EXPECT_EQ(aRay.Direct.y(), 4.0); + EXPECT_EQ(aRay.Direct.z(), 8.0); +} + +TEST(BVH_RayTest, NegativeDirection) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(-2.0, -4.0, -1.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), -2.0); + EXPECT_EQ(aRay.Direct.y(), -4.0); + EXPECT_EQ(aRay.Direct.z(), -1.0); +} + +TEST(BVH_RayTest, ZeroComponent) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(1.0, 0.0, 1.0); // Zero Y component + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 1.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 1.0); +} + +TEST(BVH_RayTest, AllZeroDirection) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(0.0, 0.0, 0.0); + + BVH_Ray aRay(aOrigin, aDirection); + + // All components should be zero + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, DefaultConstructor) +{ + BVH_Ray aRay; + + // Default constructed ray should have zero origin and direction + EXPECT_EQ(aRay.Origin.x(), 0.0); + EXPECT_EQ(aRay.Origin.y(), 0.0); + EXPECT_EQ(aRay.Origin.z(), 0.0); + + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, Ray2D) +{ + BVH_Vec2d aOrigin(1.0, 2.0); + BVH_Vec2d aDirection(3.0, 4.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Origin.x(), 1.0); + EXPECT_EQ(aRay.Origin.y(), 2.0); + + EXPECT_EQ(aRay.Direct.x(), 3.0); + EXPECT_EQ(aRay.Direct.y(), 4.0); +} + +TEST(BVH_RayTest, NormalizedDirection) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(1.0, 0.0, 0.0); // Unit vector along X + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 1.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, VerySmallDirection) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(1e-20, 1e-20, 1e-20); + + BVH_Ray aRay(aOrigin, aDirection); + + // Should handle very small values correctly + EXPECT_EQ(aRay.Direct.x(), 1e-20); + EXPECT_EQ(aRay.Direct.y(), 1e-20); + EXPECT_EQ(aRay.Direct.z(), 1e-20); +} + +TEST(BVH_RayTest, MixedZeroNonZero) +{ + BVH_Vec3d aOrigin(1.0, 2.0, 3.0); + BVH_Vec3d aDirection(0.0, 2.0, 0.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 2.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, FloatPrecision) +{ + BVH_Vec3f aOrigin(1.0f, 2.0f, 3.0f); + BVH_Vec3f aDirection(2.0f, 4.0f, 8.0f); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 2.0f); + EXPECT_EQ(aRay.Direct.y(), 4.0f); + EXPECT_EQ(aRay.Direct.z(), 8.0f); +} + +TEST(BVH_RayTest, ConstexprConstructor) +{ + // This test verifies that the constructor is truly constexpr + constexpr BVH_Vec3d aOrigin(1.0, 2.0, 3.0); + constexpr BVH_Vec3d aDirection(1.0, 1.0, 1.0); + constexpr BVH_Ray aRay(aOrigin, aDirection); + + // If this compiles, constexpr works + EXPECT_EQ(aRay.Origin.x(), 1.0); +} + +TEST(BVH_RayTest, DiagonalRay) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(1.0, 1.0, 1.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 1.0); + EXPECT_EQ(aRay.Direct.y(), 1.0); + EXPECT_EQ(aRay.Direct.z(), 1.0); +} + +TEST(BVH_RayTest, NegativeOrigin) +{ + BVH_Vec3d aOrigin(-10.0, -20.0, -30.0); + BVH_Vec3d aDirection(1.0, 2.0, 3.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Origin.x(), -10.0); + EXPECT_EQ(aRay.Origin.y(), -20.0); + EXPECT_EQ(aRay.Origin.z(), -30.0); + + EXPECT_EQ(aRay.Direct.x(), 1.0); + EXPECT_EQ(aRay.Direct.y(), 2.0); + EXPECT_EQ(aRay.Direct.z(), 3.0); +} + +TEST(BVH_RayTest, AxisAlignedX) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(1.0, 0.0, 0.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 1.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, AxisAlignedY) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(0.0, 1.0, 0.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 1.0); + EXPECT_EQ(aRay.Direct.z(), 0.0); +} + +TEST(BVH_RayTest, AxisAlignedZ) +{ + BVH_Vec3d aOrigin(0.0, 0.0, 0.0); + BVH_Vec3d aDirection(0.0, 0.0, 1.0); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 0.0); + EXPECT_EQ(aRay.Direct.y(), 0.0); + EXPECT_EQ(aRay.Direct.z(), 1.0); +} + +TEST(BVH_RayTest, LargeValues) +{ + BVH_Vec3d aOrigin(1e6, 2e6, 3e6); + BVH_Vec3d aDirection(1e3, 2e3, 4e3); + + BVH_Ray aRay(aOrigin, aDirection); + + EXPECT_EQ(aRay.Direct.x(), 1e3); + EXPECT_EQ(aRay.Direct.y(), 2e3); + EXPECT_EQ(aRay.Direct.z(), 4e3); +} + +TEST(BVH_RayTest, OriginAndDirectionPreservation) +{ + BVH_Vec3d aOrigin(1.0, 2.0, 3.0); + BVH_Vec3d aDirection(2.0, 4.0, 8.0); + + BVH_Ray aRay(aOrigin, aDirection); + + // Verify that both origin and direction are preserved exactly + EXPECT_EQ(aRay.Origin.x(), aOrigin.x()); + EXPECT_EQ(aRay.Origin.y(), aOrigin.y()); + EXPECT_EQ(aRay.Origin.z(), aOrigin.z()); + + EXPECT_EQ(aRay.Direct.x(), aDirection.x()); + EXPECT_EQ(aRay.Direct.y(), aDirection.y()); + EXPECT_EQ(aRay.Direct.z(), aDirection.z()); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_SpatialMedianBuilder_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_SpatialMedianBuilder_Test.cxx new file mode 100644 index 0000000000..baffdbbea9 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_SpatialMedianBuilder_Test.cxx @@ -0,0 +1,412 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include + +// Helper function to compute bounding box for a set +template +BVH_Box ComputeSetBox(BVH_Set* theSet) +{ + BVH_Box aBox; + const Standard_Integer aSize = theSet->Size(); + for (Standard_Integer i = 0; i < aSize; ++i) + { + aBox.Combine(theSet->Box(i)); + } + return aBox; +} + +// ============================================================================= +// BVH_SpatialMedianBuilder Basic Tests +// ============================================================================= + +TEST(BVH_SpatialMedianBuilderTest, DefaultConstructor) +{ + BVH_SpatialMedianBuilder aBuilder; + + EXPECT_EQ(aBuilder.LeafNodeSize(), 5); + EXPECT_EQ(aBuilder.MaxTreeDepth(), 32); +} + +TEST(BVH_SpatialMedianBuilderTest, CustomParameters) +{ + BVH_SpatialMedianBuilder aBuilder(10, 20, Standard_True); + + EXPECT_EQ(aBuilder.LeafNodeSize(), 10); + EXPECT_EQ(aBuilder.MaxTreeDepth(), 20); +} + +TEST(BVH_SpatialMedianBuilderTest, BuildEmptySet) +{ + BVH_Triangulation aTriangulation; + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_EQ(aTree.Length(), 0); +} + +TEST(BVH_SpatialMedianBuilderTest, BuildSingleTriangle) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_EQ(aTree.Length(), 1); + EXPECT_TRUE(aTree.IsOuter(0)); + EXPECT_EQ(aTree.NbPrimitives(0), 1); +} + +TEST(BVH_SpatialMedianBuilderTest, BuildMultipleTriangles) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 10; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_GT(aTree.Length(), 0); + EXPECT_GE(aTree.Depth(), 1); +} + +TEST(BVH_SpatialMedianBuilderTest, LeafNodeSizeRespected) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 20; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + const Standard_Integer aLeafSize = 3; + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder(aLeafSize, 32); + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + // Check all leaf nodes respect the leaf size + for (Standard_Integer i = 0; i < aTree.Length(); ++i) + { + if (aTree.IsOuter(i)) + { + EXPECT_LE(aTree.NbPrimitives(i), aLeafSize); + } + } +} + +TEST(BVH_SpatialMedianBuilderTest, MaxDepthRespected) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 100; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + const Standard_Integer aMaxDepth = 5; + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder(1, aMaxDepth); + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_LE(aTree.Depth(), aMaxDepth); +} + +TEST(BVH_SpatialMedianBuilderTest, SplitsAtSpatialMedian) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 8; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder(2, 32); + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + // For uniformly distributed data, spatial median should create balanced splits + // Root node should have roughly equal children + if (aTree.Length() > 0 && !aTree.IsOuter(0)) + { + Standard_Integer aLeftChild = aTree.Child<0>(0); + Standard_Integer aRightChild = aTree.Child<1>(0); + + if (aLeftChild != -1 && aRightChild != -1) + { + Standard_Integer aLeftPrims = aTree.NbPrimitives(aLeftChild); + Standard_Integer aRightPrims = aTree.NbPrimitives(aRightChild); + + // For 8 triangles, expect roughly 4-4 split (allowing some tolerance) + EXPECT_GE(aLeftPrims, 2); + EXPECT_LE(aLeftPrims, 6); + EXPECT_GE(aRightPrims, 2); + EXPECT_LE(aRightPrims, 6); + } + } +} + +TEST(BVH_SpatialMedianBuilderTest, Build2D) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 10; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec2d(x, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec2d(x + 0.5, 1.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec2d(x + 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_GT(aTree.Length(), 0); + EXPECT_GE(aTree.Depth(), 1); +} + +TEST(BVH_SpatialMedianBuilderTest, UseMainAxisFlag) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 20; + + // Create elongated distribution along X axis + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i * 10); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTreeMainAxis; + BVH_SpatialMedianBuilder aBuilderMainAxis(5, 32, Standard_True); + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilderMainAxis.Build(&aTriangulation, &aTreeMainAxis, aBox); + + EXPECT_GT(aTreeMainAxis.Length(), 0); +} + +TEST(BVH_SpatialMedianBuilderTest, CompareWithBinnedBuilder) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 50; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + // Build with SpatialMedianBuilder (uses 2 bins) + BVH_Tree aTreeMedian; + BVH_SpatialMedianBuilder aBuilderMedian; + aBuilderMedian.Build(&aTriangulation, &aTreeMedian, aBox); + + // Build with BinnedBuilder with 2 bins (should be identical) + BVH_Tree aTreeBinned; + BVH_BinnedBuilder aBuilderBinned; + aBuilderBinned.Build(&aTriangulation, &aTreeBinned, aBox); + + // Both should produce trees (exact structure may vary but both should be valid) + EXPECT_EQ(aTreeMedian.Length(), aTreeBinned.Length()); + EXPECT_EQ(aTreeMedian.Depth(), aTreeBinned.Depth()); +} + +TEST(BVH_SpatialMedianBuilderTest, ClusteredData) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 30; + + // Create two clusters: 15 triangles at x=0, 15 at x=100 + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = (i < 15) ? 0.0 : 100.0; + Standard_Real y = static_cast(i % 15); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, y, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, y + 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, y, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + // Spatial median should split the two clusters + EXPECT_GT(aTree.Length(), 0); + EXPECT_GE(aTree.Depth(), 1); +} + +TEST(BVH_SpatialMedianBuilderTest, FloatPrecision) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 10; + + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_ShortReal x = static_cast(i); + + BVH::Array::Append(aTriangulation.Vertices, + NCollection_Vec3(x, 0.0f, 0.0f)); + BVH::Array::Append( + aTriangulation.Vertices, + NCollection_Vec3(x + 0.5f, 1.0f, 0.0f)); + BVH::Array::Append( + aTriangulation.Vertices, + NCollection_Vec3(x + 1.0f, 0.0f, 0.0f)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_GT(aTree.Length(), 0); +} + +TEST(BVH_SpatialMedianBuilderTest, IdenticalBoxes) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 10; + + // All triangles at the same position + for (int i = 0; i < aNumTriangles; ++i) + { + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + // Should create a single leaf containing all triangles + EXPECT_GT(aTree.Length(), 0); +} + +TEST(BVH_SpatialMedianBuilderTest, LargeDataSet) +{ + BVH_Triangulation aTriangulation; + const int aNumTriangles = 500; + + // Random-ish distribution + for (int i = 0; i < aNumTriangles; ++i) + { + Standard_Real x = static_cast((i * 7) % 100); + Standard_Real y = static_cast((i * 11) % 100); + Standard_Real z = static_cast((i * 13) % 100); + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, y, z)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, y + 1.0, z)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, y, z + 1.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_Tree aTree; + BVH_SpatialMedianBuilder aBuilder; + + BVH_Box aBox = ComputeSetBox(&aTriangulation); + aBuilder.Build(&aTriangulation, &aTree, aBox); + + EXPECT_GT(aTree.Length(), 0); + EXPECT_GT(aTree.Depth(), 5); // Large dataset should produce deeper tree + + // Estimate SAH quality + Standard_Real aSAH = aTree.EstimateSAH(); + EXPECT_GT(aSAH, 0.0); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_SweepPlaneBuilder_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_SweepPlaneBuilder_Test.cxx new file mode 100644 index 0000000000..32e62b0792 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_SweepPlaneBuilder_Test.cxx @@ -0,0 +1,320 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include + +// Helper to compute bounding box for BVH_Set (avoids cached empty box issue) +template +BVH_Box ComputeSetBox(BVH_Set* theSet) +{ + BVH_Box aBox; + const Standard_Integer aSize = theSet->Size(); + for (Standard_Integer i = 0; i < aSize; ++i) + { + aBox.Combine(theSet->Box(i)); + } + return aBox; +} + +// ============================================================================= +// BVH_SweepPlaneBuilder Construction Tests +// ============================================================================= + +TEST(BVH_SweepPlaneBuilderTest, DefaultConstructor) +{ + BVH_SweepPlaneBuilder aBuilder; + EXPECT_EQ(aBuilder.LeafNodeSize(), BVH_Constants_LeafNodeSizeDefault); + EXPECT_EQ(aBuilder.MaxTreeDepth(), BVH_Constants_MaxTreeDepth); +} + +TEST(BVH_SweepPlaneBuilderTest, CustomParameters) +{ + BVH_SweepPlaneBuilder aBuilder(5, 20, 1); + EXPECT_EQ(aBuilder.LeafNodeSize(), 5); + EXPECT_EQ(aBuilder.MaxTreeDepth(), 20); +} + +// ============================================================================= +// BVH Building Tests +// ============================================================================= + +TEST(BVH_SweepPlaneBuilderTest, BuildEmptyTriangulation) +{ + BVH_Triangulation aTriangulation; + BVH_SweepPlaneBuilder aBuilder; + + BVH_Tree aBVH; + BVH_Box aBox; + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + EXPECT_EQ(aBVH.Length(), 0); +} + +TEST(BVH_SweepPlaneBuilderTest, BuildSingleTriangle) +{ + BVH_Triangulation aTriangulation; + + // Add vertices + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + + // Add triangle + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_SweepPlaneBuilder aBuilder(1, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Single triangle should create one leaf node + EXPECT_EQ(aBVH.Length(), 1); + EXPECT_TRUE(aBVH.IsOuter(0)); + EXPECT_EQ(aBVH.BegPrimitive(0), 0); + EXPECT_EQ(aBVH.EndPrimitive(0), 0); +} + +TEST(BVH_SweepPlaneBuilderTest, BuildTwoTriangles) +{ + BVH_Triangulation aTriangulation; + + // Create two triangles forming a quad + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 2, 3, 0)); + + BVH_SweepPlaneBuilder aBuilder(1, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Two triangles with leaf size 1 should create internal nodes + EXPECT_GT(aBVH.Length(), 2); + EXPECT_GE(aBVH.Depth(), 1); +} + +TEST(BVH_SweepPlaneBuilderTest, BuildMultipleTriangles_Grid) +{ + BVH_Triangulation aTriangulation; + + // Create a 2x2 grid of quads (8 triangles) + const int aGridSize = 3; + for (int i = 0; i < aGridSize; ++i) + { + for (int j = 0; j < aGridSize; ++j) + { + BVH::Array::Append( + aTriangulation.Vertices, + BVH_Vec3d(static_cast(i), static_cast(j), 0.0)); + } + } + + // Add triangles for the grid + for (int i = 0; i < aGridSize - 1; ++i) + { + for (int j = 0; j < aGridSize - 1; ++j) + { + int aV0 = i * aGridSize + j; + int aV1 = i * aGridSize + (j + 1); + int aV2 = (i + 1) * aGridSize + (j + 1); + int aV3 = (i + 1) * aGridSize + j; + + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(aV0, aV1, aV2, 0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(aV0, aV2, aV3, 0)); + } + } + + BVH_SweepPlaneBuilder aBuilder(2, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + int aTotalTriangles = (aGridSize - 1) * (aGridSize - 1) * 2; + EXPECT_GT(aBVH.Length(), 0); + EXPECT_GE(aBVH.Depth(), 1); + + // Verify tree bounds contain all triangles + BVH_Box aRootBox(aBVH.MinPoint(0), aBVH.MaxPoint(0)); + for (int i = 0; i < aTotalTriangles; ++i) + { + BVH_Box aTriBox = aTriangulation.Box(i); + EXPECT_FALSE(aRootBox.IsOut(aTriBox)); + } +} + +TEST(BVH_SweepPlaneBuilderTest, SplitAlongDifferentAxes) +{ + BVH_Triangulation aTriangulation; + + // Create triangles spread along X axis (should prefer X-axis split) + for (int i = 0; i < 10; ++i) + { + Standard_Real x = static_cast(i) * 2.0; + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_SweepPlaneBuilder aBuilder(1, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Should create splits along X axis + EXPECT_GT(aBVH.Length(), 1); + EXPECT_GE(aBVH.Depth(), 1); +} + +TEST(BVH_SweepPlaneBuilderTest, DegenerateCase_AllSamePosition) +{ + BVH_Triangulation aTriangulation; + + // All triangles at same position (degenerate case) + for (int i = 0; i < 5; ++i) + { + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.1, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.1, 0.0)); + + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_SweepPlaneBuilder aBuilder(1, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Should still build valid tree even with degenerate geometry + EXPECT_GT(aBVH.Length(), 0); +} + +// ============================================================================= +// SAH Quality Tests +// ============================================================================= + +TEST(BVH_SweepPlaneBuilderTest, SAHQuality_VerifyBetter) +{ + BVH_Triangulation aTriangulation; + + // Create well-separated triangle groups (should result in good SAH) + // Group 1: left side + for (int i = 0; i < 5; ++i) + { + Standard_Real x = static_cast(i) * 0.5; + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.4, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.2, 0.4, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + // Group 2: right side (spatially separated) + for (int i = 0; i < 5; ++i) + { + Standard_Real x = 10.0 + static_cast(i) * 0.5; + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.4, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.2, 0.4, 0.0)); + BVH::Array::Append( + aTriangulation.Elements, + BVH_Vec4i((5 + i) * 3, (5 + i) * 3 + 1, (5 + i) * 3 + 2, 0)); + } + + BVH_SweepPlaneBuilder aBuilder(2, 32, 1); + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Spatially separated groups should create clear split + EXPECT_GE(aBVH.Depth(), 1); + + // Root should have two children covering separate regions + if (!aBVH.IsOuter(0)) + { + int aLeftChild = aBVH.template Child<0>(0); + int aRightChild = aBVH.template Child<1>(0); + + BVH_Box aLeftBox(aBVH.MinPoint(aLeftChild), aBVH.MaxPoint(aLeftChild)); + BVH_Box aRightBox(aBVH.MinPoint(aRightChild), aBVH.MaxPoint(aRightChild)); + + // Boxes should not significantly overlap for well-separated geometry + Standard_Real aOverlap = 0.0; + for (int i = 0; i < 3; ++i) + { + Standard_Real aMin = std::max(aLeftBox.CornerMin()[i], aRightBox.CornerMin()[i]); + Standard_Real aMax = std::min(aLeftBox.CornerMax()[i], aRightBox.CornerMax()[i]); + if (aMax > aMin) + { + aOverlap += (aMax - aMin); + } + } + + // Overlap should be minimal for well-separated groups + Standard_Real aTotalSize = (aBox.CornerMax()[0] - aBox.CornerMin()[0]); + EXPECT_LT(aOverlap, aTotalSize * 0.1); // Less than 10% overlap + } +} + +// ============================================================================= +// Leaf Size Tests +// ============================================================================= + +TEST(BVH_SweepPlaneBuilderTest, LeafSize_RespectMaxSize) +{ + BVH_Triangulation aTriangulation; + + // Create 20 triangles + for (int i = 0; i < 20; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.9, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 0.9, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + BVH_SweepPlaneBuilder aBuilder(5, 32, 1); // Leaf size = 5 + BVH_Tree aBVH; + BVH_Box aBox = ComputeSetBox(&aTriangulation); + + aBuilder.Build(&aTriangulation, &aBVH, aBox); + + // Verify leaf nodes don't exceed the specified size + for (int i = 0; i < aBVH.Length(); ++i) + { + if (aBVH.IsOuter(i)) + { + int aLeafSize = aBVH.EndPrimitive(i) - aBVH.BegPrimitive(i) + 1; + EXPECT_LE(aLeafSize, 5); + } + } +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Tools_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Tools_Test.cxx new file mode 100644 index 0000000000..7503d61f4c --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Tools_Test.cxx @@ -0,0 +1,813 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include +#include + +TEST(BVH_ToolsTest, PointBoxSquareDistance) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Point inside box + Standard_Real aDist1 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(0.5, 0.5, 0.5), aBox); + EXPECT_NEAR(aDist1, 0.0, Precision::Confusion()); + + // Point on face + Standard_Real aDist2 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(0.5, 0.5, 1.0), aBox); + EXPECT_NEAR(aDist2, 0.0, Precision::Confusion()); + + // Point outside box (distance = 1) + Standard_Real aDist3 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(2.0, 0.5, 0.5), aBox); + EXPECT_NEAR(aDist3, 1.0, Precision::Confusion()); + + // Point at corner outside (distance^2 = 1 + 1 + 1 = 3) + Standard_Real aDist4 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(2.0, 2.0, 2.0), aBox); + EXPECT_NEAR(aDist4, 3.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistance) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 1.0)); + BVH_Box aBox3(BVH_Vec3d(0.5, 0.5, 0.5), BVH_Vec3d(1.5, 1.5, 1.5)); + + // Separated boxes + Standard_Real aDist1 = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist1, 1.0, Precision::Confusion()); + + // Overlapping boxes + Standard_Real aDist2 = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox3); + EXPECT_NEAR(aDist2, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxProjection) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Point inside - should return same point + BVH_Vec3d aProj1 = + BVH_Tools::PointBoxProjection(BVH_Vec3d(0.5, 0.5, 0.5), aBox); + EXPECT_NEAR(aProj1.x(), 0.5, Precision::Confusion()); + EXPECT_NEAR(aProj1.y(), 0.5, Precision::Confusion()); + EXPECT_NEAR(aProj1.z(), 0.5, Precision::Confusion()); + + // Point outside - should clamp to box + BVH_Vec3d aProj2 = + BVH_Tools::PointBoxProjection(BVH_Vec3d(2.0, 0.5, 0.5), aBox); + EXPECT_NEAR(aProj2.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj2.y(), 0.5, Precision::Confusion()); + EXPECT_NEAR(aProj2.z(), 0.5, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersection) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray hitting the box + Standard_Boolean aHit1 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit1); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); + + // Ray missing the box + Standard_Boolean aHit2 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 5.0, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); + + // Ray starting inside box + Standard_Boolean aHit3 = BVH_Tools::RayBoxIntersection(BVH_Vec3d(0.5, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit3); + EXPECT_LE(aTimeEnter, 0.0); + EXPECT_NEAR(aTimeLeave, 0.5, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionParallelRay) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray parallel to X-axis, passing through box + Standard_Boolean aHit1 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit1); + + // Ray parallel to X-axis, missing box (Y out of range) + Standard_Boolean aHit2 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 2.0, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); +} + +TEST(BVH_ToolsTest, PointTriangleProjection) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(1.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 1.0, 0.0); + + // Point projects to vertex + BVH_Vec3d aProj1 = + BVH_Tools::PointTriangleProjection(BVH_Vec3d(-1.0, -1.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj1.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj1.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj1.z(), 0.0, Precision::Confusion()); + + // Point projects to edge + BVH_Vec3d aProj2 = BVH_Tools::PointTriangleProjection(BVH_Vec3d(0.5, -1.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj2.x(), 0.5, Precision::Confusion()); + EXPECT_NEAR(aProj2.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj2.z(), 0.0, Precision::Confusion()); + + // Point projects inside triangle + BVH_Vec3d aProj3 = + BVH_Tools::PointTriangleProjection(BVH_Vec3d(0.25, 0.25, 1.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj3.x(), 0.25, Precision::Confusion()); + EXPECT_NEAR(aProj3.y(), 0.25, Precision::Confusion()); + EXPECT_NEAR(aProj3.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistance2D) +{ + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + + // Point inside box + Standard_Real aDist1 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec2d(0.5, 0.5), aBox); + EXPECT_NEAR(aDist1, 0.0, Precision::Confusion()); + + // Point outside box + Standard_Real aDist2 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec2d(2.0, 0.5), aBox); + EXPECT_NEAR(aDist2, 1.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionDiagonalRay) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Diagonal ray through box center + BVH_Vec3d aDir(1.0, 1.0, 1.0); + Standard_Real aNorm = std::sqrt(3.0); + aDir = BVH_Vec3d(aDir.x() / aNorm, aDir.y() / aNorm, aDir.z() / aNorm); + + Standard_Boolean aHit = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, -1.0, -1.0), + aDir, + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_GT(aTimeEnter, 0.0); + EXPECT_GT(aTimeLeave, aTimeEnter); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionNegativeDirection) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray going in negative X direction + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(2.0, 0.5, 0.5), + BVH_Vec3d(-1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionTouchingEdge) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray touching edge of box + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 0.0, 0.0), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionTouchingCorner) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray through corner + BVH_Vec3d aDir(1.0, 1.0, 1.0); + Standard_Real aNorm = std::sqrt(3.0); + aDir = BVH_Vec3d(aDir.x() / aNorm, aDir.y() / aNorm, aDir.z() / aNorm); + + Standard_Boolean aHit = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, -1.0, -1.0), + aDir, + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceTouching) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(1.0, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0)); + + // Touching boxes (sharing a face) + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceOneInsideOther) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(10.0, 10.0, 10.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(3.0, 3.0, 3.0)); + + // Small box inside large box + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceCornerToCorner) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 2.0), BVH_Vec3d(3.0, 3.0, 3.0)); + + // Distance from corner to corner: sqrt(1^2 + 1^2 + 1^2) = sqrt(3), squared = 3 + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 3.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxProjectionNegativeCoords) +{ + BVH_Box aBox(BVH_Vec3d(-1.0, -1.0, -1.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Point far outside in negative direction + BVH_Vec3d aProj = + BVH_Tools::PointBoxProjection(BVH_Vec3d(-5.0, -5.0, -5.0), aBox); + EXPECT_NEAR(aProj.x(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), -1.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionOnEdge01) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(2.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 2.0, 0.0); + + // Point projects onto edge between Node0 and Node1 + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(1.0, -1.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionOnEdge12) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(2.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 2.0, 0.0); + + // Point projects onto edge between Node1 and Node2 + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(2.0, 2.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionOnEdge20) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(2.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 2.0, 0.0); + + // Point projects onto edge between Node2 and Node0 + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(-1.0, 1.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionVertex1) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(2.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 2.0, 0.0); + + // Point projects to Node1 + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(3.0, -1.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionVertex2) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(2.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 2.0, 0.0); + + // Point projects to Node2 + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(-1.0, 3.0, 0.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjection3D) +{ + // Triangle in 3D space (not in XY plane) + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(1.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 1.0, 1.0); + + // Point above triangle center + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(0.3, 0.3, 0.8), + aNode0, + aNode1, + aNode2); + + // Should project inside triangle + EXPECT_GE(aProj.x(), 0.0); + EXPECT_GE(aProj.y(), 0.0); + EXPECT_LE(aProj.x() + aProj.y(), 1.0 + Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistance2D) +{ + BVH_Box aBox1(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + BVH_Box aBox2(BVH_Vec2d(3.0, 0.0), BVH_Vec2d(4.0, 1.0)); + + // Distance = 2 + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 4.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxProjection2D) +{ + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + + // Point outside + BVH_Vec2d aProj = BVH_Tools::PointBoxProjection(BVH_Vec2d(2.0, 2.0), aBox); + EXPECT_NEAR(aProj.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 1.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistanceAtVertex) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Point on vertex + Standard_Real aDist = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(0.0, 0.0, 0.0), aBox); + EXPECT_NEAR(aDist, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistanceAtEdge) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Point on edge + Standard_Real aDist = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(0.5, 0.0, 0.0), aBox); + EXPECT_NEAR(aDist, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistanceFloat) +{ + BVH_Box aBox(BVH_Vec3f(0.0f, 0.0f, 0.0f), BVH_Vec3f(1.0f, 1.0f, 1.0f)); + + // Point inside box + Standard_ShortReal aDist1 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3f(0.5f, 0.5f, 0.5f), aBox); + EXPECT_NEAR(aDist1, 0.0f, 1e-5f); + + // Point outside box + Standard_ShortReal aDist2 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3f(2.0f, 0.5f, 0.5f), aBox); + EXPECT_NEAR(aDist2, 1.0f, 1e-5f); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionFloat) +{ + BVH_Box aBox(BVH_Vec3f(0.0f, 0.0f, 0.0f), BVH_Vec3f(1.0f, 1.0f, 1.0f)); + + Standard_ShortReal aTimeEnter, aTimeLeave; + + // Ray hitting the box + Standard_Boolean aHit = + BVH_Tools::RayBoxIntersection(BVH_Vec3f(-1.0f, 0.5f, 0.5f), + BVH_Vec3f(1.0f, 0.0f, 0.0f), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_NEAR(aTimeEnter, 1.0f, 1e-5f); + EXPECT_NEAR(aTimeLeave, 2.0f, 1e-5f); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionBehindRay) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray pointing away from box + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(2.0, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + // The box is behind the ray origin + EXPECT_TRUE(aTimeLeave < 0.0 || !aHit); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionYAxis) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray along Y axis + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(0.5, -1.0, 0.5), + BVH_Vec3d(0.0, 1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionZAxis) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray along Z axis + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(0.5, 0.5, -1.0), + BVH_Vec3d(0.0, 0.0, 1.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceFloat) +{ + BVH_Box aBox1(BVH_Vec3f(0.0f, 0.0f, 0.0f), BVH_Vec3f(1.0f, 1.0f, 1.0f)); + BVH_Box aBox2(BVH_Vec3f(2.0f, 0.0f, 0.0f), BVH_Vec3f(3.0f, 1.0f, 1.0f)); + + Standard_ShortReal aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 1.0f, 1e-5f); +} + +TEST(BVH_ToolsTest, PointBoxProjectionFloat) +{ + BVH_Box aBox(BVH_Vec3f(0.0f, 0.0f, 0.0f), BVH_Vec3f(1.0f, 1.0f, 1.0f)); + + BVH_Vec3f aProj = + BVH_Tools::PointBoxProjection(BVH_Vec3f(2.0f, 0.5f, 0.5f), aBox); + EXPECT_NEAR(aProj.x(), 1.0f, 1e-5f); + EXPECT_NEAR(aProj.y(), 0.5f, 1e-5f); + EXPECT_NEAR(aProj.z(), 0.5f, 1e-5f); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistanceNegativeBox) +{ + BVH_Box aBox(BVH_Vec3d(-2.0, -2.0, -2.0), BVH_Vec3d(-1.0, -1.0, -1.0)); + + // Point at origin + Standard_Real aDist = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(0.0, 0.0, 0.0), aBox); + // Distance = sqrt(1^2 + 1^2 + 1^2) = sqrt(3), squared = 3 + EXPECT_NEAR(aDist, 3.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceEdgeToEdge) +{ + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 2.0, 0.0), BVH_Vec3d(3.0, 3.0, 1.0)); + + // Closest points are on edges, distance = sqrt(1^2 + 1^2) = sqrt(2), squared = 2 + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox1, aBox2); + EXPECT_NEAR(aDist, 2.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionFloat) +{ + BVH_Vec3f aNode0(0.0f, 0.0f, 0.0f); + BVH_Vec3f aNode1(1.0f, 0.0f, 0.0f); + BVH_Vec3f aNode2(0.0f, 1.0f, 0.0f); + + // Point projects inside triangle + BVH_Vec3f aProj = + BVH_Tools::PointTriangleProjection(BVH_Vec3f(0.25f, 0.25f, 1.0f), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 0.25f, 1e-5f); + EXPECT_NEAR(aProj.y(), 0.25f, 1e-5f); + EXPECT_NEAR(aProj.z(), 0.0f, 1e-5f); +} + +TEST(BVH_ToolsTest, PointBoxSquareDistanceLargeBox) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1000.0, 1000.0, 1000.0)); + + // Point inside + Standard_Real aDist1 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(500.0, 500.0, 500.0), aBox); + EXPECT_NEAR(aDist1, 0.0, Precision::Confusion()); + + // Point outside + Standard_Real aDist2 = + BVH_Tools::PointBoxSquareDistance(BVH_Vec3d(1001.0, 500.0, 500.0), aBox); + EXPECT_NEAR(aDist2, 1.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionLargeBox) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1000.0, 1000.0, 1000.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + Standard_Boolean aHit = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-100.0, 500.0, 500.0), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit); + EXPECT_NEAR(aTimeEnter, 100.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 1100.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointTriangleProjectionCentroid) +{ + BVH_Vec3d aNode0(0.0, 0.0, 0.0); + BVH_Vec3d aNode1(3.0, 0.0, 0.0); + BVH_Vec3d aNode2(0.0, 3.0, 0.0); + + // Point directly above centroid + BVH_Vec3d aProj = BVH_Tools::PointTriangleProjection(BVH_Vec3d(1.0, 1.0, 5.0), + aNode0, + aNode1, + aNode2); + EXPECT_NEAR(aProj.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.y(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj.z(), 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, BoxBoxSquareDistanceSameBox) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Distance to itself should be 0 + Standard_Real aDist = BVH_Tools::BoxBoxSquareDistance(aBox, aBox); + EXPECT_NEAR(aDist, 0.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, PointBoxProjectionAllCorners) +{ + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + // Test projection from each octant + BVH_Vec3d aProj1 = + BVH_Tools::PointBoxProjection(BVH_Vec3d(-1.0, -1.0, -1.0), aBox); + EXPECT_NEAR(aProj1.x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj1.y(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aProj1.z(), 0.0, Precision::Confusion()); + + BVH_Vec3d aProj2 = + BVH_Tools::PointBoxProjection(BVH_Vec3d(2.0, 2.0, 2.0), aBox); + EXPECT_NEAR(aProj2.x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj2.y(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aProj2.z(), 1.0, Precision::Confusion()); +} + +// ======================================================================================= +// Tests for improved RayBoxIntersection (dimension-independent, early exit) +// ======================================================================================= + +TEST(BVH_ToolsTest, RayBoxIntersection2D) +{ + // Test 2D ray-box intersection (old code was hardcoded for 3D) + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + + Standard_Real aTimeEnter, aTimeLeave; + + // Ray hitting the box + Standard_Boolean aHit1 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(-1.0, 0.5), + BVH_Vec2d(1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit1); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); + + // Ray missing the box + Standard_Boolean aHit2 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(-1.0, 2.0), + BVH_Vec2d(1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); + + // Ray parallel to X axis, inside Y bounds + Standard_Boolean aHit3 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(-1.0, 0.5), + BVH_Vec2d(1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit3); + + // Ray parallel to Y axis, inside X bounds + Standard_Boolean aHit4 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(0.5, -1.0), + BVH_Vec2d(0.0, 1.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit4); + EXPECT_NEAR(aTimeEnter, 1.0, Precision::Confusion()); + EXPECT_NEAR(aTimeLeave, 2.0, Precision::Confusion()); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionEarlyExit) +{ + // Test early exit optimization when ray misses on first axis + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + Standard_Real aTimeEnter, aTimeLeave; + + // Ray clearly misses in X direction - should exit immediately + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(-2.0, 0.5, 0.5), + BVH_Vec3d(-1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit); + + // Ray with early mismatch in Y direction + Standard_Boolean aHit2 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(0.5, -2.0, 0.5), + BVH_Vec3d(0.0, -1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); +} + +TEST(BVH_ToolsTest, RayBoxIntersectionParallelRayEarlyExit) +{ + // Test parallel ray that misses - should exit immediately + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + Standard_Real aTimeEnter, aTimeLeave; + + // Ray parallel to X axis but Y coordinate outside box - should reject immediately + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, 2.0, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit); + + // Ray parallel to Y axis but X coordinate outside box + Standard_Boolean aHit2 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(2.0, -1.0, 0.5), + BVH_Vec3d(0.0, 1.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); + + // Ray parallel to Z axis but X and Y outside + Standard_Boolean aHit3 = + BVH_Tools::RayBoxIntersection(BVH_Vec3d(-1.0, -1.0, 0.5), + BVH_Vec3d(0.0, 0.0, 1.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit3); +} + +TEST(BVH_ToolsTest, RayBoxIntersection2DParallelBothAxes) +{ + // 2D test with ray parallel to both axes (direction = 0,0) + BVH_Box aBox(BVH_Vec2d(0.0, 0.0), BVH_Vec2d(1.0, 1.0)); + Standard_Real aTimeEnter, aTimeLeave; + + // Ray origin inside box, direction = 0 + Standard_Boolean aHit1 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(0.5, 0.5), + BVH_Vec2d(0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit1); // Should still hit because origin is inside + + // Ray origin outside box, direction = 0 + Standard_Boolean aHit2 = BVH_Tools::RayBoxIntersection(BVH_Vec2d(2.0, 2.0), + BVH_Vec2d(0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit2); // Should miss because origin is outside +} + +TEST(BVH_ToolsTest, RayBoxIntersectionNegativeTime) +{ + // Test that ray doesn't report intersection behind the origin + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + Standard_Real aTimeEnter, aTimeLeave; + + // Ray origin is past the box, pointing away + Standard_Boolean aHit = BVH_Tools::RayBoxIntersection(BVH_Vec3d(2.0, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_FALSE(aHit); + + // Ray origin inside box, pointing away - should still hit (leave point is in front) + Standard_Boolean aHit2 = BVH_Tools::RayBoxIntersection(BVH_Vec3d(0.5, 0.5, 0.5), + BVH_Vec3d(1.0, 0.0, 0.0), + aBox, + aTimeEnter, + aTimeLeave); + EXPECT_TRUE(aHit2); + EXPECT_TRUE(aTimeLeave >= 0.0); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Traverse_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Traverse_Test.cxx new file mode 100644 index 0000000000..498cb41420 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Traverse_Test.cxx @@ -0,0 +1,683 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include +#include + +#include + +// ======================================================================================= +// Test implementations of BVH_Traverse +// ======================================================================================= + +//! Simple traverse implementation that counts all elements +class BVH_CountAllElements : public BVH_Traverse +{ +public: + BVH_CountAllElements() + : myAcceptedCount(0) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt&, + const BVH_VecNt&, + Standard_Real& theMetric) const Standard_OVERRIDE + { + theMetric = 0.0; // All nodes have same metric + return Standard_False; // Never reject + } + + virtual Standard_Boolean Accept(const Standard_Integer, const Standard_Real&) Standard_OVERRIDE + { + ++myAcceptedCount; + return Standard_True; + } + + Standard_Integer AcceptedCount() const { return myAcceptedCount; } + + void Reset() { myAcceptedCount = 0; } + +private: + mutable Standard_Integer myAcceptedCount; +}; + +//! Traverse that rejects nodes outside a bounding box +class BVH_BoxSelector : public BVH_Traverse +{ +public: + BVH_BoxSelector(const BVH_Box& theBox) + : myBox(theBox), + myAcceptedCount(0) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt& theMin, + const BVH_VecNt& theMax, + Standard_Real& theMetric) const Standard_OVERRIDE + { + // Reject if box doesn't intersect with selection box + theMetric = 0.0; + return myBox.IsOut(theMin, theMax); + } + + virtual Standard_Boolean Accept(const Standard_Integer, const Standard_Real&) Standard_OVERRIDE + { + ++myAcceptedCount; + return Standard_True; + } + + Standard_Integer AcceptedCount() const { return myAcceptedCount; } + + void Reset() { myAcceptedCount = 0; } + +private: + BVH_Box myBox; + mutable Standard_Integer myAcceptedCount; +}; + +//! Traverse with distance-based metric and early termination +class BVH_DistanceSelector : public BVH_Traverse +{ +public: + BVH_DistanceSelector(const BVH_Vec3d& thePoint, Standard_Real theMaxDist) + : myPoint(thePoint), + myMaxDistSq(theMaxDist * theMaxDist), + myMinDistSq(std::numeric_limits::max()), + myAcceptedCount(0), + myClosestIndex(-1) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt& theMin, + const BVH_VecNt& theMax, + Standard_Real& theMetric) const Standard_OVERRIDE + { + // Compute squared distance from point to box + theMetric = PointBoxSquareDistance(myPoint, theMin, theMax); + return theMetric > myMaxDistSq; + } + + virtual Standard_Boolean Accept(const Standard_Integer theIndex, + const Standard_Real& theMetric) Standard_OVERRIDE + { + ++myAcceptedCount; + if (theMetric < myMinDistSq) + { + myMinDistSq = theMetric; + myClosestIndex = theIndex; + } + return Standard_True; + } + + virtual Standard_Boolean IsMetricBetter(const Standard_Real& theLeft, + const Standard_Real& theRight) const Standard_OVERRIDE + { + return theLeft < theRight; // Closer is better + } + + virtual Standard_Boolean RejectMetric(const Standard_Real& theMetric) const Standard_OVERRIDE + { + return theMetric > myMaxDistSq; + } + + Standard_Integer AcceptedCount() const { return myAcceptedCount; } + + Standard_Integer ClosestIndex() const { return myClosestIndex; } + + Standard_Real MinDistance() const { return std::sqrt(myMinDistSq); } + +private: + static Standard_Real PointBoxSquareDistance(const BVH_Vec3d& thePoint, + const BVH_Vec3d& theMin, + const BVH_Vec3d& theMax) + { + Standard_Real aDist = 0.0; + for (int i = 0; i < 3; ++i) + { + if (thePoint[i] < theMin[i]) + { + Standard_Real d = theMin[i] - thePoint[i]; + aDist += d * d; + } + else if (thePoint[i] > theMax[i]) + { + Standard_Real d = thePoint[i] - theMax[i]; + aDist += d * d; + } + } + return aDist; + } + + BVH_Vec3d myPoint; + Standard_Real myMaxDistSq; + Standard_Real myMinDistSq; + Standard_Integer myAcceptedCount; + Standard_Integer myClosestIndex; +}; + +//! Traverse with early stopping after finding N elements +class BVH_LimitedSelector : public BVH_Traverse +{ +public: + BVH_LimitedSelector(Standard_Integer theMaxCount) + : myMaxCount(theMaxCount), + myAcceptedCount(0) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt&, + const BVH_VecNt&, + Standard_Real& theMetric) const Standard_OVERRIDE + { + theMetric = 0.0; + return Standard_False; + } + + virtual Standard_Boolean Accept(const Standard_Integer, const Standard_Real&) Standard_OVERRIDE + { + ++myAcceptedCount; + return Standard_True; + } + + virtual Standard_Boolean Stop() const Standard_OVERRIDE { return myAcceptedCount >= myMaxCount; } + + Standard_Integer AcceptedCount() const { return myAcceptedCount; } + +private: + Standard_Integer myMaxCount; + Standard_Integer myAcceptedCount; +}; + +// ======================================================================================= +// Test implementations of BVH_PairTraverse +// ======================================================================================= + +//! Simple pair traverse that counts all pairs +class BVH_CountAllPairs : public BVH_PairTraverse +{ +public: + BVH_CountAllPairs() + : myAcceptedCount(0) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt&, + const BVH_VecNt&, + const BVH_VecNt&, + const BVH_VecNt&, + Standard_Real& theMetric) const Standard_OVERRIDE + { + theMetric = 0.0; + return Standard_False; // Never reject + } + + virtual Standard_Boolean Accept(const Standard_Integer, const Standard_Integer) Standard_OVERRIDE + { + ++myAcceptedCount; + return Standard_True; + } + + Standard_Integer AcceptedCount() const { return myAcceptedCount; } + +private: + mutable Standard_Integer myAcceptedCount; +}; + +//! Pair traverse that only accepts overlapping boxes +class BVH_OverlapDetector : public BVH_PairTraverse +{ +public: + BVH_OverlapDetector() + : myOverlapCount(0), + myRejectCount(0) + { + } + + virtual Standard_Boolean RejectNode(const BVH_VecNt& theMin1, + const BVH_VecNt& theMax1, + const BVH_VecNt& theMin2, + const BVH_VecNt& theMax2, + Standard_Real& theMetric) const Standard_OVERRIDE + { + ++myRejectCount; + theMetric = 0.0; + // Reject if boxes don't overlap + BVH_Box aBox1(theMin1, theMax1); + Standard_Boolean isOut = aBox1.IsOut(theMin2, theMax2); + return isOut; + } + + virtual Standard_Boolean Accept(const Standard_Integer, const Standard_Integer) Standard_OVERRIDE + { + // For this test, if we reach Accept, it means the bounding boxes overlap. + // In a real implementation, you would check actual triangle-triangle intersection here. + // For testing purposes, we count all pairs whose bounding boxes overlap. + ++myOverlapCount; + return Standard_True; + } + + Standard_Integer OverlapCount() const { return myOverlapCount; } + + Standard_Integer RejectCount() const { return myRejectCount; } + +private: + mutable Standard_Integer myOverlapCount; + mutable Standard_Integer myRejectCount; +}; + +// ======================================================================================= +// Helper functions +// ======================================================================================= + +//! Creates a simple triangulation for testing +opencascade::handle> CreateSimpleTriangulationBVH( + Standard_Integer theNumTriangles) +{ + BVH_Triangulation aTriangulation; + + for (Standard_Integer i = 0; i < theNumTriangles; ++i) + { + Standard_Real x = static_cast(i * 2); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 2.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + opencascade::handle> aBVH = new BVH_Tree; + BVH_BinnedBuilder aBuilder; + aBuilder.Build(&aTriangulation, aBVH.get(), aTriangulation.Box()); + + return aBVH; +} + +// ======================================================================================= +// Tests for BVH_Traverse +// ======================================================================================= + +TEST(BVH_TraverseTest, CountAllElements) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + BVH_CountAllElements aSelector; + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 10); + EXPECT_EQ(aSelector.AcceptedCount(), 10); +} + +TEST(BVH_TraverseTest, EmptyTree) +{ + opencascade::handle> aBVH = new BVH_Tree; + + BVH_CountAllElements aSelector; + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_TraverseTest, NullTree) +{ + opencascade::handle> aBVH; + + BVH_CountAllElements aSelector; + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_TraverseTest, BoxSelection) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + // Select elements in the first half + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, -1.0), BVH_Vec3d(10.0, 2.0, 1.0)); + BVH_BoxSelector aSelector(aBox); + Standard_Integer aCount = aSelector.Select(aBVH); + + // Should select approximately half of the elements + EXPECT_GT(aCount, 0); + EXPECT_LT(aCount, 10); +} + +TEST(BVH_TraverseTest, EmptyBoxSelection) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + // Select with box that doesn't intersect any elements + BVH_Box aBox(BVH_Vec3d(100.0, 100.0, 100.0), BVH_Vec3d(200.0, 200.0, 200.0)); + BVH_BoxSelector aSelector(aBox); + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_TraverseTest, FullBoxSelection) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + // Select with box that contains all elements + BVH_Box aBox(BVH_Vec3d(-100.0, -100.0, -100.0), BVH_Vec3d(100.0, 100.0, 100.0)); + BVH_BoxSelector aSelector(aBox); + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 10); +} + +TEST(BVH_TraverseTest, DistanceBasedSelection) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + // Find elements near a point + BVH_Vec3d aPoint(5.0, 0.5, 0.0); + Standard_Real aMaxDist = 5.0; + BVH_DistanceSelector aSelector(aPoint, aMaxDist); + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_GT(aCount, 0); + EXPECT_LE(aCount, 10); + EXPECT_GE(aSelector.ClosestIndex(), 0); + EXPECT_LT(aSelector.ClosestIndex(), 10); +} + +TEST(BVH_TraverseTest, EarlyTermination) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(100); + + // Stop after finding 5 elements + BVH_LimitedSelector aSelector(5); + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 5); + EXPECT_EQ(aSelector.AcceptedCount(), 5); +} + +TEST(BVH_TraverseTest, LargeDataSet) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(1000); + + BVH_CountAllElements aSelector; + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 1000); +} + +TEST(BVH_TraverseTest, MetricBasedPruning) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(50); + + // Very restrictive distance should result in few acceptances + BVH_Vec3d aPoint(1000.0, 1000.0, 1000.0); // Far away + Standard_Real aMaxDist = 1.0; // Small radius + BVH_DistanceSelector aSelector(aPoint, aMaxDist); + Standard_Integer aCount = aSelector.Select(aBVH); + + EXPECT_EQ(aCount, 0); // Nothing should be within range +} + +// ======================================================================================= +// Tests for BVH_PairTraverse +// ======================================================================================= + +TEST(BVH_PairTraverseTest, IsOutVerification) +{ + // Verify that IsOut works correctly for non-overlapping boxes + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 0.0)); + BVH_Box aBox2(BVH_Vec3d(100.0, 0.0, 0.0), BVH_Vec3d(103.0, 1.0, 0.0)); + + // These boxes are far apart and should not overlap + EXPECT_TRUE(aBox1.IsOut(aBox2.CornerMin(), aBox2.CornerMax())); + EXPECT_TRUE(aBox2.IsOut(aBox1.CornerMin(), aBox1.CornerMax())); +} + +TEST(BVH_PairTraverseTest, TriangulationBoxVerification) +{ + // Create two triangulations and verify their bounding boxes don't overlap + BVH_Triangulation aTri1, aTri2; + + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri1.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = 100.0 + static_cast(i); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri2.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + // Mark as dirty to force bounding box computation + aTri1.MarkDirty(); + aTri2.MarkDirty(); + + BVH_Box aBox1 = aTri1.Box(); + BVH_Box aBox2 = aTri2.Box(); + + // Verify the boxes are where we expect them + EXPECT_TRUE(aBox1.IsValid()); + EXPECT_TRUE(aBox2.IsValid()); + + // Box1 should be roughly [0, 0, 0] to [3, 1, 0] + EXPECT_NEAR(aBox1.CornerMin().x(), 0.0, 0.01); + EXPECT_NEAR(aBox1.CornerMax().x(), 3.0, 0.01); + + // Box2 should be roughly [100, 0, 0] to [103, 1, 0] + EXPECT_NEAR(aBox2.CornerMin().x(), 100.0, 0.01); + EXPECT_NEAR(aBox2.CornerMax().x(), 103.0, 0.01); + + // The boxes should not overlap + EXPECT_TRUE(aBox1.IsOut(aBox2)); + EXPECT_TRUE(aBox2.IsOut(aBox1)); +} + +TEST(BVH_PairTraverseTest, BVHRootBoxVerification) +{ + // Create triangulations and verify their BVH root boxes + BVH_Triangulation aTri1, aTri2; + + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri1.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = 100.0 + static_cast(i); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri2.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + aTri1.MarkDirty(); + aTri2.MarkDirty(); + + opencascade::handle> aBVH1 = new BVH_Tree; + opencascade::handle> aBVH2 = new BVH_Tree; + + BVH_BinnedBuilder aBuilder; + aBuilder.Build(&aTri1, aBVH1.get(), aTri1.Box()); + aBuilder.Build(&aTri2, aBVH2.get(), aTri2.Box()); + + // Check the root node bounding boxes + BVH_Vec3d aMin1 = aBVH1->MinPoint(0); + BVH_Vec3d aMax1 = aBVH1->MaxPoint(0); + BVH_Vec3d aMin2 = aBVH2->MinPoint(0); + BVH_Vec3d aMax2 = aBVH2->MaxPoint(0); + + // Verify root boxes are separated + EXPECT_NEAR(aMin1.x(), 0.0, 0.01); + EXPECT_NEAR(aMax1.x(), 3.0, 0.01); + EXPECT_NEAR(aMin2.x(), 100.0, 0.01); + EXPECT_NEAR(aMax2.x(), 103.0, 0.01); + + // The root boxes should not overlap + BVH_Box aBox1(aMin1, aMax1); + EXPECT_TRUE(aBox1.IsOut(aMin2, aMax2)); + + // Check if the nodes are inner or leaf nodes + const BVH_Vec4i& aData1 = aBVH1->NodeInfoBuffer()[0]; + const BVH_Vec4i& aData2 = aBVH2->NodeInfoBuffer()[0]; + + // aData.x() == 0 means inner node, != 0 means leaf node + // With 3 triangles, the tree might be a single leaf node + EXPECT_EQ(aData1.x(), 1) << "Root of BVH1 should be a leaf node (3 elements)"; + EXPECT_EQ(aData2.x(), 1) << "Root of BVH2 should be a leaf node (3 elements)"; +} + +TEST(BVH_PairTraverseTest, CountAllPairs) +{ + opencascade::handle> aBVH1 = CreateSimpleTriangulationBVH(5); + opencascade::handle> aBVH2 = CreateSimpleTriangulationBVH(5); + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 25); // 5 x 5 pairs +} + +TEST(BVH_PairTraverseTest, EmptyFirstTree) +{ + opencascade::handle> aBVH1 = new BVH_Tree; + opencascade::handle> aBVH2 = CreateSimpleTriangulationBVH(5); + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_PairTraverseTest, EmptySecondTree) +{ + opencascade::handle> aBVH1 = CreateSimpleTriangulationBVH(5); + opencascade::handle> aBVH2 = new BVH_Tree; + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_PairTraverseTest, NullTrees) +{ + opencascade::handle> aBVH1; + opencascade::handle> aBVH2; + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 0); +} + +TEST(BVH_PairTraverseTest, OverlapDetection_SameTrees) +{ + opencascade::handle> aBVH = CreateSimpleTriangulationBVH(10); + + BVH_OverlapDetector aSelector; + Standard_Integer aCount = aSelector.Select(aBVH, aBVH); + + // Self-overlap: all 10 elements overlap with themselves + EXPECT_GE(aCount, 10); +} + +TEST(BVH_PairTraverseTest, OverlapDetection_NonOverlapping) +{ + // Create two triangulations in different regions + BVH_Triangulation aTri1, aTri2; + + // First triangulation at x=0..5 + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri1.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri1.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + // Second triangulation at x=100..105 (far away) + for (Standard_Integer i = 0; i < 3; ++i) + { + Standard_Real x = 100.0 + static_cast(i); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTri2.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTri2.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + // Mark as dirty to force bounding box computation + aTri1.MarkDirty(); + aTri2.MarkDirty(); + + opencascade::handle> aBVH1 = new BVH_Tree; + opencascade::handle> aBVH2 = new BVH_Tree; + + BVH_BinnedBuilder aBuilder; + aBuilder.Build(&aTri1, aBVH1.get(), aTri1.Box()); + aBuilder.Build(&aTri2, aBVH2.get(), aTri2.Box()); + + BVH_OverlapDetector aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + // Debug: Check how many times RejectNode was called + // If it's 0, RejectNode is not being called at all + // If it's > 0 but overlaps are still found, then IsOut is broken + EXPECT_GT(aSelector.RejectCount(), 0) << "RejectNode should be called at least once"; + + // No overlaps expected + EXPECT_EQ(aCount, 0) << "Found " << aCount << " overlaps (RejectNode called " + << aSelector.RejectCount() << " times)"; +} + +TEST(BVH_PairTraverseTest, AsymmetricPairs) +{ + opencascade::handle> aBVH1 = CreateSimpleTriangulationBVH(3); + opencascade::handle> aBVH2 = CreateSimpleTriangulationBVH(7); + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 21); // 3 x 7 pairs +} + +TEST(BVH_PairTraverseTest, LargeDataSets) +{ + opencascade::handle> aBVH1 = CreateSimpleTriangulationBVH(50); + opencascade::handle> aBVH2 = CreateSimpleTriangulationBVH(50); + + BVH_CountAllPairs aSelector; + Standard_Integer aCount = aSelector.Select(aBVH1, aBVH2); + + EXPECT_EQ(aCount, 2500); // 50 x 50 pairs +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Tree_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Tree_Test.cxx new file mode 100644 index 0000000000..68777b1bb2 --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Tree_Test.cxx @@ -0,0 +1,399 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include +#include +#include + +TEST(BVH_TreeTest, DefaultConstructor) +{ + BVH_Tree aTree; + + EXPECT_EQ(aTree.Length(), 0); + EXPECT_EQ(aTree.Depth(), 0); +} + +TEST(BVH_TreeTest, AddLeafNode) +{ + BVH_Tree aTree; + + BVH_Vec3d aMin(0.0, 0.0, 0.0); + BVH_Vec3d aMax(1.0, 1.0, 1.0); + + int aNodeIndex = aTree.AddLeafNode(aMin, aMax, 0, 10); + + EXPECT_EQ(aNodeIndex, 0); + EXPECT_EQ(aTree.Length(), 1); + EXPECT_TRUE(aTree.IsOuter(0)); + EXPECT_EQ(aTree.BegPrimitive(0), 0); + EXPECT_EQ(aTree.EndPrimitive(0), 10); + EXPECT_EQ(aTree.NbPrimitives(0), 11); +} + +TEST(BVH_TreeTest, AddInnerNode) +{ + BVH_Tree aTree; + + // Add two leaf nodes first + BVH_Vec3d aMin1(0.0, 0.0, 0.0); + BVH_Vec3d aMax1(1.0, 1.0, 1.0); + int aLeaf1 = aTree.AddLeafNode(aMin1, aMax1, 0, 5); + + BVH_Vec3d aMin2(2.0, 0.0, 0.0); + BVH_Vec3d aMax2(3.0, 1.0, 1.0); + int aLeaf2 = aTree.AddLeafNode(aMin2, aMax2, 6, 10); + + // Add inner node + BVH_Vec3d aMinRoot(0.0, 0.0, 0.0); + BVH_Vec3d aMaxRoot(3.0, 1.0, 1.0); + int aRoot = aTree.AddInnerNode(aMinRoot, aMaxRoot, aLeaf1, aLeaf2); + + EXPECT_EQ(aTree.Length(), 3); + EXPECT_FALSE(aTree.IsOuter(aRoot)); + EXPECT_EQ(aTree.template Child<0>(aRoot), aLeaf1); + EXPECT_EQ(aTree.template Child<1>(aRoot), aLeaf2); +} + +TEST(BVH_TreeTest, MinMaxPoints) +{ + BVH_Tree aTree; + + BVH_Vec3d aMin(1.0, 2.0, 3.0); + BVH_Vec3d aMax(4.0, 5.0, 6.0); + + aTree.AddLeafNode(aMin, aMax, 0, 0); + + EXPECT_NEAR(aTree.MinPoint(0).x(), 1.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MinPoint(0).y(), 2.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MinPoint(0).z(), 3.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MaxPoint(0).x(), 4.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MaxPoint(0).y(), 5.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MaxPoint(0).z(), 6.0, Precision::Confusion()); +} + +TEST(BVH_TreeTest, Clear) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + aTree.AddLeafNode(BVH_Vec3d(1.0, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0), 1, 1); + + EXPECT_EQ(aTree.Length(), 2); + + aTree.Clear(); + + EXPECT_EQ(aTree.Length(), 0); + EXPECT_EQ(aTree.Depth(), 0); +} + +TEST(BVH_TreeTest, Reserve) +{ + BVH_Tree aTree; + + // Reserve should not throw + aTree.Reserve(100); + + // Can still add nodes after reserve + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + EXPECT_EQ(aTree.Length(), 1); +} + +TEST(BVH_TreeTest, SetOuterInner) +{ + BVH_Tree aTree; + + // Add leaf and change to inner + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + + EXPECT_TRUE(aTree.IsOuter(0)); + + aTree.SetInner(0); + EXPECT_FALSE(aTree.IsOuter(0)); + + aTree.SetOuter(0); + EXPECT_TRUE(aTree.IsOuter(0)); +} + +TEST(BVH_TreeTest, Level) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + + // Default level is 0 + EXPECT_EQ(aTree.Level(0), 0); + + // Change level + aTree.Level(0) = 5; + EXPECT_EQ(aTree.Level(0), 5); +} + +TEST(BVH_TreeTest, AddLeafNodeWithBox) +{ + BVH_Tree aTree; + + BVH_Box aBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + + int aNodeIndex = aTree.AddLeafNode(aBox, 0, 5); + + EXPECT_EQ(aNodeIndex, 0); + EXPECT_TRUE(aTree.IsOuter(0)); + EXPECT_EQ(aTree.BegPrimitive(0), 0); + EXPECT_EQ(aTree.EndPrimitive(0), 5); +} + +TEST(BVH_TreeTest, AddInnerNodeWithBox) +{ + BVH_Tree aTree; + + BVH_Box aBox1(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0)); + BVH_Box aBox2(BVH_Vec3d(2.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 1.0)); + BVH_Box aRootBox(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 1.0)); + + int aLeaf1 = aTree.AddLeafNode(aBox1, 0, 5); + int aLeaf2 = aTree.AddLeafNode(aBox2, 6, 10); + int aRoot = aTree.AddInnerNode(aRootBox, aLeaf1, aLeaf2); + + EXPECT_FALSE(aTree.IsOuter(aRoot)); + EXPECT_EQ(aTree.template Child<0>(aRoot), aLeaf1); + EXPECT_EQ(aTree.template Child<1>(aRoot), aLeaf2); +} + +TEST(BVH_TreeTest, EstimateSAH) +{ + BVH_Tree aTree; + + // Create a simple tree with root and two leaves + BVH_Vec3d aMin1(0.0, 0.0, 0.0); + BVH_Vec3d aMax1(1.0, 1.0, 1.0); + int aLeaf1 = aTree.AddLeafNode(aMin1, aMax1, 0, 0); + + BVH_Vec3d aMin2(2.0, 0.0, 0.0); + BVH_Vec3d aMax2(3.0, 1.0, 1.0); + int aLeaf2 = aTree.AddLeafNode(aMin2, aMax2, 1, 1); + + BVH_Vec3d aMinRoot(0.0, 0.0, 0.0); + BVH_Vec3d aMaxRoot(3.0, 1.0, 1.0); + aTree.AddInnerNode(aMinRoot, aMaxRoot, aLeaf1, aLeaf2); + + Standard_Real aSAH = aTree.EstimateSAH(); + + // SAH should be positive + EXPECT_GT(aSAH, 0.0); +} + +TEST(BVH_TreeTest, NbPrimitives) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 5, 15); + + // NbPrimitives = EndPrimitive - BegPrimitive + 1 = 15 - 5 + 1 = 11 + EXPECT_EQ(aTree.NbPrimitives(0), 11); +} + +TEST(BVH_TreeTest, SinglePrimitive) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 7, 7); + + EXPECT_EQ(aTree.NbPrimitives(0), 1); + EXPECT_EQ(aTree.BegPrimitive(0), 7); + EXPECT_EQ(aTree.EndPrimitive(0), 7); +} + +TEST(BVH_TreeTest, Float3DTree) +{ + BVH_Tree aTree; + + BVH_Vec3f aMin(0.0f, 0.0f, 0.0f); + BVH_Vec3f aMax(1.0f, 1.0f, 1.0f); + + aTree.AddLeafNode(aMin, aMax, 0, 5); + + EXPECT_EQ(aTree.Length(), 1); + EXPECT_NEAR(aTree.MinPoint(0).x(), 0.0f, 1e-5f); + EXPECT_NEAR(aTree.MaxPoint(0).x(), 1.0f, 1e-5f); +} + +TEST(BVH_TreeTest, Tree2D) +{ + BVH_Tree aTree; + + BVH_Vec2d aMin(0.0, 0.0); + BVH_Vec2d aMax(1.0, 1.0); + + aTree.AddLeafNode(aMin, aMax, 0, 5); + + EXPECT_EQ(aTree.Length(), 1); + EXPECT_NEAR(aTree.MinPoint(0).x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MinPoint(0).y(), 0.0, Precision::Confusion()); +} + +TEST(BVH_TreeTest, Tree4D) +{ + BVH_Tree aTree; + + BVH_Vec4d aMin(0.0, 0.0, 0.0, 0.0); + BVH_Vec4d aMax(1.0, 1.0, 1.0, 1.0); + + aTree.AddLeafNode(aMin, aMax, 0, 5); + + EXPECT_EQ(aTree.Length(), 1); + EXPECT_NEAR(aTree.MinPoint(0).x(), 0.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MinPoint(0).w(), 0.0, Precision::Confusion()); +} + +TEST(BVH_TreeTest, MultipleLeaves) +{ + BVH_Tree aTree; + + for (int i = 0; i < 10; ++i) + { + BVH_Vec3d aMin(i * 1.0, 0.0, 0.0); + BVH_Vec3d aMax(i * 1.0 + 1.0, 1.0, 1.0); + aTree.AddLeafNode(aMin, aMax, i, i); + } + + EXPECT_EQ(aTree.Length(), 10); + + for (int i = 0; i < 10; ++i) + { + EXPECT_TRUE(aTree.IsOuter(i)); + EXPECT_EQ(aTree.BegPrimitive(i), i); + EXPECT_EQ(aTree.EndPrimitive(i), i); + } +} + +TEST(BVH_TreeTest, DeepTree) +{ + BVH_Tree aTree; + + // Create leaves + BVH_Vec3d aMin1(0.0, 0.0, 0.0); + BVH_Vec3d aMax1(1.0, 1.0, 1.0); + int aLeaf1 = aTree.AddLeafNode(aMin1, aMax1, 0, 0); + + BVH_Vec3d aMin2(1.0, 0.0, 0.0); + BVH_Vec3d aMax2(2.0, 1.0, 1.0); + int aLeaf2 = aTree.AddLeafNode(aMin2, aMax2, 1, 1); + + BVH_Vec3d aMin3(2.0, 0.0, 0.0); + BVH_Vec3d aMax3(3.0, 1.0, 1.0); + int aLeaf3 = aTree.AddLeafNode(aMin3, aMax3, 2, 2); + + BVH_Vec3d aMin4(3.0, 0.0, 0.0); + BVH_Vec3d aMax4(4.0, 1.0, 1.0); + int aLeaf4 = aTree.AddLeafNode(aMin4, aMax4, 3, 3); + + // Create intermediate nodes + BVH_Vec3d aMinI1(0.0, 0.0, 0.0); + BVH_Vec3d aMaxI1(2.0, 1.0, 1.0); + int aInner1 = aTree.AddInnerNode(aMinI1, aMaxI1, aLeaf1, aLeaf2); + + BVH_Vec3d aMinI2(2.0, 0.0, 0.0); + BVH_Vec3d aMaxI2(4.0, 1.0, 1.0); + int aInner2 = aTree.AddInnerNode(aMinI2, aMaxI2, aLeaf3, aLeaf4); + + // Create root + BVH_Vec3d aMinRoot(0.0, 0.0, 0.0); + BVH_Vec3d aMaxRoot(4.0, 1.0, 1.0); + int aRoot = aTree.AddInnerNode(aMinRoot, aMaxRoot, aInner1, aInner2); + + EXPECT_EQ(aTree.Length(), 7); + EXPECT_FALSE(aTree.IsOuter(aRoot)); + EXPECT_FALSE(aTree.IsOuter(aInner1)); + EXPECT_FALSE(aTree.IsOuter(aInner2)); + EXPECT_TRUE(aTree.IsOuter(aLeaf1)); + EXPECT_TRUE(aTree.IsOuter(aLeaf4)); +} + +TEST(BVH_TreeTest, ModifyPrimitiveIndices) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 5); + + // Modify primitive indices + aTree.BegPrimitive(0) = 10; + aTree.EndPrimitive(0) = 20; + + EXPECT_EQ(aTree.BegPrimitive(0), 10); + EXPECT_EQ(aTree.EndPrimitive(0), 20); + EXPECT_EQ(aTree.NbPrimitives(0), 11); +} + +TEST(BVH_TreeTest, ModifyMinMaxPoints) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + + // Modify min/max points + aTree.MinPoint(0) = BVH_Vec3d(-1.0, -1.0, -1.0); + aTree.MaxPoint(0) = BVH_Vec3d(2.0, 2.0, 2.0); + + EXPECT_NEAR(aTree.MinPoint(0).x(), -1.0, Precision::Confusion()); + EXPECT_NEAR(aTree.MaxPoint(0).x(), 2.0, Precision::Confusion()); +} + +TEST(BVH_TreeTest, ChangeChild) +{ + BVH_Tree aTree; + + int aLeaf1 = aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + int aLeaf2 = aTree.AddLeafNode(BVH_Vec3d(1.0, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0), 1, 1); + int aLeaf3 = aTree.AddLeafNode(BVH_Vec3d(2.0, 0.0, 0.0), BVH_Vec3d(3.0, 1.0, 1.0), 2, 2); + int aRoot = + aTree.AddInnerNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(2.0, 1.0, 1.0), aLeaf1, aLeaf2); + + EXPECT_EQ(aTree.template Child<0>(aRoot), aLeaf1); + EXPECT_EQ(aTree.template Child<1>(aRoot), aLeaf2); + + // Change children + aTree.template ChangeChild<0>(aRoot) = aLeaf3; + + EXPECT_EQ(aTree.template Child<0>(aRoot), aLeaf3); + EXPECT_EQ(aTree.template Child<1>(aRoot), aLeaf2); +} + +TEST(BVH_TreeTest, NodeInfoBuffer) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 5, 10); + + // Access node info buffer + const BVH_Array4i& aBuffer = aTree.NodeInfoBuffer(); + int aSize = BVH::Array::Size(aBuffer); + EXPECT_EQ(aSize, 1); +} + +TEST(BVH_TreeTest, MinMaxPointBuffers) +{ + BVH_Tree aTree; + + aTree.AddLeafNode(BVH_Vec3d(0.0, 0.0, 0.0), BVH_Vec3d(1.0, 1.0, 1.0), 0, 0); + + // Access point buffers + const auto& aMinBuffer = aTree.MinPointBuffer(); + const auto& aMaxBuffer = aTree.MaxPointBuffer(); + + int aMinSize = BVH::Array::Size(aMinBuffer); + int aMaxSize = BVH::Array::Size(aMaxBuffer); + EXPECT_EQ(aMinSize, 1); + EXPECT_EQ(aMaxSize, 1); +} diff --git a/src/FoundationClasses/TKMath/GTests/BVH_Triangulation_Test.cxx b/src/FoundationClasses/TKMath/GTests/BVH_Triangulation_Test.cxx new file mode 100644 index 0000000000..484244237e --- /dev/null +++ b/src/FoundationClasses/TKMath/GTests/BVH_Triangulation_Test.cxx @@ -0,0 +1,330 @@ +// Copyright (c) 2025 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include + +#include + +// ============================================================================= +// BVH_Triangulation Basic Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, DefaultConstructor) +{ + BVH_Triangulation aTriangulation; + + EXPECT_EQ(aTriangulation.Size(), 0); + EXPECT_EQ((BVH::Array::Size(aTriangulation.Vertices)), 0); + EXPECT_EQ((BVH::Array::Size(aTriangulation.Elements)), 0); +} + +TEST(BVH_TriangulationTest, AddSingleTriangle) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + EXPECT_EQ(aTriangulation.Size(), 1); + EXPECT_EQ((BVH::Array::Size(aTriangulation.Vertices)), 3); +} + +TEST(BVH_TriangulationTest, AddMultipleTriangles) +{ + BVH_Triangulation aTriangulation; + + for (int i = 0; i < 5; ++i) + { + Standard_Real x = static_cast(i); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + EXPECT_EQ(aTriangulation.Size(), 5); + EXPECT_EQ((BVH::Array::Size(aTriangulation.Vertices)), 15); +} + +// ============================================================================= +// BVH_Triangulation Box Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, BoxForSingleTriangle) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 2.0, 3.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(4.0, 1.0, 2.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 3.0, 1.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_Box aBox = aTriangulation.Box(0); + + // Min point should be componentwise min of all vertices + EXPECT_NEAR(aBox.CornerMin().x(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().y(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().z(), 1.0, 1e-10); + + // Max point should be componentwise max of all vertices + EXPECT_NEAR(aBox.CornerMax().x(), 4.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().y(), 3.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().z(), 3.0, 1e-10); +} + +TEST(BVH_TriangulationTest, BoxForDegenerateTriangle) +{ + BVH_Triangulation aTriangulation; + + // Triangle with all vertices at same point (degenerate) + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 1.0, 1.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 1.0, 1.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 1.0, 1.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_Box aBox = aTriangulation.Box(0); + + EXPECT_NEAR(aBox.CornerMin().x(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().y(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().z(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().y(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().z(), 1.0, 1e-10); +} + +TEST(BVH_TriangulationTest, BoxForFlatTriangle) +{ + BVH_Triangulation aTriangulation; + + // Triangle flat in XY plane (Z = 0) + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 3.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_Box aBox = aTriangulation.Box(0); + + EXPECT_NEAR(aBox.CornerMin().z(), 0.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().z(), 0.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().x(), 2.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().y(), 3.0, 1e-10); +} + +// ============================================================================= +// BVH_Triangulation Center Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, CenterComputation) +{ + BVH_Triangulation aTriangulation; + + // Triangle with vertices at (0,0,0), (3,0,0), (0,3,0) + // Centroid should be at (1,1,0) + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(3.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 3.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + EXPECT_NEAR(aTriangulation.Center(0, 0), 1.0, 1e-10); // X centroid + EXPECT_NEAR(aTriangulation.Center(0, 1), 1.0, 1e-10); // Y centroid + EXPECT_NEAR(aTriangulation.Center(0, 2), 0.0, 1e-10); // Z centroid +} + +TEST(BVH_TriangulationTest, CenterAlongXAxis) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(2.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(3.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + // Centroid X = (1 + 2 + 3) / 3 = 2.0 + EXPECT_NEAR(aTriangulation.Center(0, 0), 2.0, 1e-10); +} + +TEST(BVH_TriangulationTest, CenterAlongYAxis) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 2.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 4.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 6.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + // Centroid Y = (2 + 4 + 6) / 3 = 4.0 + EXPECT_NEAR(aTriangulation.Center(0, 1), 4.0, 1e-10); +} + +TEST(BVH_TriangulationTest, CenterAlongZAxis) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 1.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 4.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 7.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + // Centroid Z = (1 + 4 + 7) / 3 = 4.0 + EXPECT_NEAR(aTriangulation.Center(0, 2), 4.0, 1e-10); +} + +// ============================================================================= +// BVH_Triangulation Swap Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, SwapTwoTriangles) +{ + BVH_Triangulation aTriangulation; + + // Triangle 0 + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + // Triangle 1 + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(5.0, 5.0, 5.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(6.0, 5.0, 5.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(5.0, 6.0, 5.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(3, 4, 5, 0)); + + Standard_Real aCentroid0Before = aTriangulation.Center(0, 0); + Standard_Real aCentroid1Before = aTriangulation.Center(1, 0); + + aTriangulation.Swap(0, 1); + + // After swap, centroids should be swapped + EXPECT_NEAR(aTriangulation.Center(0, 0), aCentroid1Before, 1e-10); + EXPECT_NEAR(aTriangulation.Center(1, 0), aCentroid0Before, 1e-10); +} + +TEST(BVH_TriangulationTest, SwapPreservesVertices) +{ + BVH_Triangulation aTriangulation; + + // Add vertices and triangles + for (int i = 0; i < 3; ++i) + { + Standard_Real x = static_cast(i) * 10.0; + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(x + 0.5, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Elements, + BVH_Vec4i(i * 3, i * 3 + 1, i * 3 + 2, 0)); + } + + Standard_Integer aVertexCount = BVH::Array::Size(aTriangulation.Vertices); + + aTriangulation.Swap(0, 2); + + // Vertex count should not change + EXPECT_EQ((BVH::Array::Size(aTriangulation.Vertices)), aVertexCount); +} + +// ============================================================================= +// BVH_Triangulation 2D Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, Triangulation2D) +{ + BVH_Triangulation aTriangulation2D; + + BVH::Array::Append(aTriangulation2D.Vertices, BVH_Vec2d(0.0, 0.0)); + BVH::Array::Append(aTriangulation2D.Vertices, BVH_Vec2d(1.0, 0.0)); + BVH::Array::Append(aTriangulation2D.Vertices, BVH_Vec2d(0.5, 1.0)); + BVH::Array::Append(aTriangulation2D.Elements, BVH_Vec4i(0, 1, 2, 0)); + + EXPECT_EQ(aTriangulation2D.Size(), 1); + + BVH_Box aBox = aTriangulation2D.Box(0); + EXPECT_NEAR(aBox.CornerMin().x(), 0.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().y(), 0.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().y(), 1.0, 1e-10); +} + +// ============================================================================= +// BVH_Triangulation Shared Vertices Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, SharedVertices) +{ + BVH_Triangulation aTriangulation; + + // Create quad with 4 vertices, 2 triangles sharing edge + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 0.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, 1.0, 0.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 1.0, 0.0)); + + // Triangle 0: (0, 1, 2) + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + // Triangle 1: (0, 2, 3) - shares vertices 0 and 2 with triangle 0 + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 2, 3, 0)); + + EXPECT_EQ(aTriangulation.Size(), 2); + EXPECT_EQ((BVH::Array::Size(aTriangulation.Vertices)), 4); + + // Both triangles should have valid boxes + BVH_Box aBox0 = aTriangulation.Box(0); + BVH_Box aBox1 = aTriangulation.Box(1); + + EXPECT_FALSE(aBox0.IsOut(aBox1)); // Should overlap +} + +// ============================================================================= +// BVH_Triangulation Float Precision Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, FloatPrecision) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3f(0.0f, 0.0f, 0.0f)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3f(1.0f, 0.0f, 0.0f)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3f(0.0f, 1.0f, 0.0f)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + EXPECT_EQ(aTriangulation.Size(), 1); + + BVH_Box aBox = aTriangulation.Box(0); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0f, 1e-6f); + EXPECT_NEAR(aBox.CornerMax().y(), 1.0f, 1e-6f); +} + +// ============================================================================= +// BVH_Triangulation Negative Coordinates Tests +// ============================================================================= + +TEST(BVH_TriangulationTest, NegativeCoordinates) +{ + BVH_Triangulation aTriangulation; + + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(-1.0, -2.0, -3.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(1.0, -1.0, -2.0)); + BVH::Array::Append(aTriangulation.Vertices, BVH_Vec3d(0.0, 0.0, -1.0)); + BVH::Array::Append(aTriangulation.Elements, BVH_Vec4i(0, 1, 2, 0)); + + BVH_Box aBox = aTriangulation.Box(0); + + EXPECT_NEAR(aBox.CornerMin().x(), -1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().y(), -2.0, 1e-10); + EXPECT_NEAR(aBox.CornerMin().z(), -3.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().x(), 1.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().y(), 0.0, 1e-10); + EXPECT_NEAR(aBox.CornerMax().z(), -1.0, 1e-10); +} diff --git a/src/FoundationClasses/TKMath/GTests/FILES.cmake b/src/FoundationClasses/TKMath/GTests/FILES.cmake index 7f3afa417b..bfd4fd84f0 100644 --- a/src/FoundationClasses/TKMath/GTests/FILES.cmake +++ b/src/FoundationClasses/TKMath/GTests/FILES.cmake @@ -7,6 +7,19 @@ set(OCCT_TKMath_GTests_FILES Bnd_BoundSortBox_Test.cxx Bnd_Box_Test.cxx Bnd_OBB_Test.cxx + BVH_BinnedBuilder_Test.cxx + BVH_Box_Test.cxx + BVH_BuildQueue_Test.cxx + BVH_LinearBuilder_Test.cxx + BVH_QuickSorter_Test.cxx + BVH_RadixSorter_Test.cxx + BVH_Ray_Test.cxx + BVH_SpatialMedianBuilder_Test.cxx + BVH_SweepPlaneBuilder_Test.cxx + BVH_Tools_Test.cxx + BVH_Traverse_Test.cxx + BVH_Triangulation_Test.cxx + BVH_Tree_Test.cxx ElCLib_Test.cxx gp_Ax3_Test.cxx gp_Mat_Test.cxx diff --git a/src/Visualization/TKV3d/Select3D/Select3D_SensitivePrimitiveArray.cxx b/src/Visualization/TKV3d/Select3D/Select3D_SensitivePrimitiveArray.cxx index 0f1baa404f..4bb5ad9f4e 100644 --- a/src/Visualization/TKV3d/Select3D/Select3D_SensitivePrimitiveArray.cxx +++ b/src/Visualization/TKV3d/Select3D/Select3D_SensitivePrimitiveArray.cxx @@ -946,8 +946,7 @@ Standard_Boolean Select3D_SensitivePrimitiveArray::overlapsElement( } const Standard_Integer aPatchSize = myBvhIndices.PatchSize(theElemIdx); - Select3D_BndBox3d aBox; - Standard_Boolean aResult = Standard_False; + Standard_Boolean aResult = Standard_False; SelectBasics_PickResult aPickResult; switch (myPrimType) {