diff --git a/CMakeLists.txt b/CMakeLists.txt index d04194ff..52951cb5 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,24 @@ else () target_link_libraries(PyGEL PRIVATE GEL) endif () +# Include the parallel-hashmap library +include(FetchContent) +# Our headers depend on it, so we need to it alongside GEL +set(PHMAP_INSTALL TRUE CACHE INTERNAL "Install parallel_hashmap") +FetchContent_Declare( + parallel-hashmap + GIT_REPOSITORY https://github.com/greg7mdp/parallel-hashmap.git + GIT_TAG v2.0.0 # adjust tag/branch/commit as needed +) +FetchContent_MakeAvailable(parallel-hashmap) +# Pass the directory to GEL during a build +target_include_directories( + GEL PUBLIC + $ + $ + ) + + # Install GEL library install(TARGETS GEL LIBRARY DESTINATION lib) diff --git a/src/GEL/CGLA/ArithMatFloat.h b/src/GEL/CGLA/ArithMatFloat.h index 005f5e86..9b96f062 100644 --- a/src/GEL/CGLA/ArithMatFloat.h +++ b/src/GEL/CGLA/ArithMatFloat.h @@ -12,6 +12,7 @@ #define CGLA_ARITHMATFLOAT_H #include +#include namespace CGLA { diff --git a/src/GEL/CGLA/ArithVec.h b/src/GEL/CGLA/ArithVec.h index ad317450..0270fee9 100644 --- a/src/GEL/CGLA/ArithVec.h +++ b/src/GEL/CGLA/ArithVec.h @@ -21,51 +21,51 @@ namespace CGLA { - + /** \brief Template representing generic arithmetic vectors. - + The three parameters to the template are - + T - the scalar type (i.e. float, int, double etc.) - + V - the name of the vector type. This template is always (and only) used as ancestor of concrete types, and the name of the class _inheriting_ _from_ this class is used as the V argument. - + N - The final argument is the dimension N. For instance, N=3 for a 3D vector. - + This class template contains all functions that are assumed to be the same for any arithmetic vector - regardless of dimension or the type of scalars used for coordinates. - + The template contains no virtual functions which is important since they add overhead. */ - + template class ArithVec { - + protected: - + /// The actual contents of the vector. std::array data; - + protected: /// @name Constructors /// @{ - + /// Construct uninitialized vector constexpr ArithVec() noexcept = default; - + /// Construct a vector where all coordinates are identical constexpr explicit ArithVec(T _a) noexcept { std::fill_n(data, N, _a); } - + /// Construct a 2D vector constexpr ArithVec(T _a, T _b) noexcept { @@ -73,7 +73,7 @@ namespace CGLA data[0] = _a; data[1] = _b; } - + /// Construct a 3D vector constexpr ArithVec(T _a, T _b, T _c) noexcept { @@ -82,7 +82,7 @@ namespace CGLA data[1] = _b; data[2] = _c; } - + /// Construct a 4D vector constexpr ArithVec(T _a, T _b, T _c, T _d) noexcept { @@ -96,19 +96,19 @@ namespace CGLA /// @} public: - + /// For convenience we define a more meaningful name for the scalar type using ScalarType = T; - + /// A more meaningful name for vector type using VectorType = V; /// @name Accessors /// @{ - + /// Return dimension of vector static constexpr unsigned int get_dim() {return N;} - + /// Set all coordinates of a 2D vector. constexpr void set(T _a, T _b) noexcept { @@ -116,7 +116,7 @@ namespace CGLA data[0] = _a; data[1] = _b; } - + /// Set all coordinates of a 3D vector. constexpr void set(T _a, T _b, T _c) noexcept { @@ -125,7 +125,7 @@ namespace CGLA data[1] = _b; data[2] = _c; } - + /// Set all coordinates of a 4D vector. constexpr void set(T _a, T _b, T _c, T _d) noexcept { @@ -135,28 +135,28 @@ namespace CGLA data[2] = _c; data[3] = _d; } - + /// Const index operator constexpr const T& operator [] ( unsigned int i ) const { assert(i::iterator; using const_iterator = typename std::array::const_iterator; - - + + /** Get a pointer to first element in data array. This function may be useful when interfacing with some other API such as OpenGL (TM) */ constexpr iterator begin() {return data.begin();} constexpr iterator end() {return data.end();} constexpr T* get() {return data.data();} - + /** Get a const pointer to first element in data array. This function may be useful when interfacing with some other API such as OpenGL (TM). */ constexpr const_iterator begin() const {return data.begin();} constexpr const_iterator end() const {return data.end();} constexpr const T* get() const {return data.data();} - + /// @} /// @name Comparison Operators /// @{ - + /// Equality operator constexpr bool operator==(const V& v) const { return std::equal(begin(),end(), v.begin()); } - + /// Equality wrt scalar. True if all coords are equal to scalar constexpr bool operator==(T k) const { return std::count(begin(),end(), k)==N; } - + /// Inequality operator constexpr bool operator!=(const V& v) const { return !(*this==v); } - + /// Inequality wrt scalar. True if any coord not equal to scalar constexpr bool operator!=(T k) const { return !(*this==k); } - - + + /// @} /// @name Comparison and geometric significance operators /// @{ - + /** Compare all coordinates against other vector ( < ) and combine with 'and'. Similar to testing whether we are on one side of three planes. */ constexpr bool all_l (const V& v) const @@ -226,7 +226,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_and<>(), std::less()); } - + /** Compare all coordinates against other vector ( <= ) and combine with 'and'. Similar to testing whether we are on one side of three planes. */ constexpr bool all_le (const V& v) const @@ -234,7 +234,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_and<>(), std::less_equal()); } - + /** Compare all coordinates against other vector ( > ) and combine with 'and'. Similar to testing whether we are on one side of three planes. */ constexpr bool all_g (const V& v) const @@ -242,7 +242,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_and<>(), std::greater()); } - + /** Compare all coordinates against other vector ( >= ) and combine with 'and'. Similar to testing whether we are on one side of three planes. */ constexpr bool all_ge (const V& v) const @@ -250,7 +250,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_and<>(), std::greater_equal()); } - + /** Compare all coordinates against other vector ( < ) and combine with 'or'. Similar to testing whether we are on one side of three planes. */ constexpr bool any_l (const V& v) const @@ -258,7 +258,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_or<>(), std::less()); } - + /** Compare all coordinates against other vector ( <= ) and combine with 'or'. Similar to testing whether we are on one side of three planes. */ constexpr bool any_le (const V& v) const @@ -266,7 +266,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_or<>(), std::less_equal()); } - + /** Compare all coordinates against other vector ( > ) and combine with 'or'. Similar to testing whether we are on one side of three planes. */ constexpr bool any_g (const V& v) const @@ -274,7 +274,7 @@ namespace CGLA return std::inner_product(begin(), end(), v.begin(), true, std::logical_or<>(), std::greater()); } - + /** Compare all coordinates against other vector ( >= ) and combine with 'or'. Similar to testing whether we are on one side of three planes. */ constexpr bool any_ge (const V& v) const @@ -283,146 +283,182 @@ namespace CGLA std::logical_or(), std::greater_equal()); } - + /// Returns true if p(x) is true for any vector element. p :: T -> bool + template + constexpr bool any(Predicate&& p) const + { + return std::any_of(begin(), end(), p); + } + + /// Returns true if p(x) is true for all vector elements. p :: T -> bool + template + constexpr bool all(Predicate&& p) const + { + return std::all_of(begin(), end(), p); + } + /// @} /// @name Assignment operators /// @{ - + /// Assignment multiplication with scalar. - constexpr const V& operator *=(T k) + constexpr V& operator *=(T k) { for(auto& x : data) {x*=k;} - return static_cast(*this); + return static_cast(*this); } - + /// Assignment division with scalar. - constexpr const V& operator /=(T k) + constexpr V& operator /=(T k) { for(auto& x : data) {x/=k;} - return static_cast(*this); + + return static_cast(*this); } - + /// Assignment addition with scalar. Adds scalar to each coordinate. - constexpr const V& operator +=(T k) + constexpr V& operator +=(T k) { for(auto& x : data) {x+=k;} - return static_cast(*this); + + return static_cast(*this); } - + /// Assignment subtraction with scalar. Subtracts scalar from each coord. - constexpr const V& operator -=(T k) + constexpr V& operator -=(T k) { for(auto& x : data) {x-=k;} - return static_cast(*this); + return static_cast(*this); } - + /** Assignment multiplication with vector. Multiply each coord independently. */ - constexpr const V& operator *=(const V& v) + constexpr V& operator *=(const V& v) { - std::transform(begin(), end(), v.begin(), begin(), std::multiplies()); - return static_cast(*this); + for (auto i = 0; i < N; ++i) { + data[i] = data[i] * v[i]; + } + return static_cast(*this); } - + /// Assigment division with vector. Each coord divided independently. - constexpr const V& operator /=(const V& v) + constexpr V& operator /=(const V& v) { - std::transform(begin(), end(), v.begin(), begin(), std::divides()); - return static_cast(*this); + for (auto i = 0; i < N; ++i) { + data[i] = data[i] / v[i]; + } + return static_cast(*this); } - + /// Assignmment addition with vector. - constexpr const V& operator +=(const V& v) + constexpr V& operator +=(const V& v) { - std::transform(begin(), end(), v.begin(), begin(), std::plus()); - return static_cast(*this); + for (auto i = 0; i < N; ++i) { + data[i] = data[i] + v[i]; + } + return static_cast(*this); } - + /// Assignment subtraction with vector. - constexpr const V& operator -=(const V& v) + constexpr V& operator -=(const V& v) { - std::transform(begin(), end(), v.begin(), begin(), std::minus()); - return static_cast(*this); + for (auto i = 0; i < N; ++i) { + data[i] = data[i] - v[i]; + } + return static_cast(*this); } - - + /// @} /// @name Unary operators /// @{ - + /// Negate vector. constexpr V operator - () const { - V v_new; - std::transform(begin(), end(), v_new.begin(), std::negate()); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = -data[i]; + } + return ret; } - + /// @} /// @name Binary operations with vectors /// @{ - + /** Multiply vector with vector. Each coord multiplied independently Do not confuse this operation with dot product. */ constexpr V operator * (const V& v1) const { - V v_new; - std::transform(begin(), end(), v1.begin(), v_new.begin(), std::multiplies()); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] * v1[i]; + } + return ret; } - + /// Add two vectors constexpr V operator + (const V& v1) const { - V v_new; - std::transform(begin(), end(), v1.begin(), v_new.begin(), std::plus()); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] + v1[i]; + } + return ret; } - + /// Subtract two vectors. constexpr V operator - (const V& v1) const { - V v_new; - std::transform(begin(), end(), v1.begin(), v_new.begin(), std::minus()); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] - v1[i]; + } + return ret; } - + /// Divide two vectors. Each coord separately constexpr V operator / (const V& v1) const { - V v_new; - std::transform(begin(), end(), v1.begin(), v_new.begin(), std::divides()); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] / v1[i]; + } + return ret; } - + /// @} /// @name Binary operations with scalars /// @{ - + /// Multiply scalar onto vector. constexpr V operator * (T k) const { - V v_new; - std::transform(begin(), end(), v_new.begin(), [k](T x){return x*k;}); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] * k; + } + return ret; } - - + + /// Divide vector by scalar. constexpr V operator / (T k) const { - V v_new; - std::transform(begin(), end(), v_new.begin(), [k](T x){return x/k;}); - return v_new; + V ret; + for (auto i = 0; i < N; ++i) { + ret[i] = data[i] / k; + } + return ret; } - - + + /// Return the smallest coordinate of the vector constexpr T min_coord() const { return *std::min_element(begin(), end()); } - + /// Return the largest coordinate of the vector constexpr T max_coord() const { @@ -430,7 +466,7 @@ namespace CGLA } }; - + template constexpr std::ostream& operator<<(std::ostream&os, const ArithVec& v) { @@ -438,8 +474,8 @@ namespace CGLA for(const T& x : v) os << x << " "; os << "]"; return os; - } - + } + /// Get from operator for ArithVec descendants. template constexpr std::istream& operator>>(std::istream&is, ArithVec& v) @@ -456,26 +492,30 @@ namespace CGLA is.ignore(); return is; } - - + + /** Dot product for two vectors. The `*' operator is reserved for coordinatewise multiplication of vectors. */ template constexpr T dot(const ArithVec& v0, const ArithVec& v1) { - return std::inner_product(v0.begin(), v0.end(), v1.begin(), T(0)); + T acc = 0; + for (auto i = 0; i < N; ++i) { + acc += v0[i] * v1[i]; + } + return acc; } - + /** Compute the sqr length by taking dot product of vector with itself. */ template constexpr T sqr_length(const ArithVec& v) { return dot(v,v); } - + /** Multiply double onto vector. This operator handles the case where the vector is on the righ side of the `*'. - + \note It seems to be optimal to put the binary operators inside the ArithVec class template, but the operator functions whose left operand is _not_ a vector cannot be inside, hence they @@ -488,13 +528,13 @@ namespace CGLA a float * ArithVec function is not found if the left operand is really a double. */ - + template constexpr V operator * (double k, const ArithVec& v) { return v * k; } - + /** Multiply float onto vector. See the note in the documentation regarding multiplication of a double onto a vector. */ template @@ -502,7 +542,7 @@ namespace CGLA { return v * k; } - + /** Multiply unsigned int onto vector. See the note in the documentation regarding multiplication of a double onto a vector. */ template @@ -510,7 +550,7 @@ namespace CGLA { return v * k; } - + /** Returns the vector containing for each coordinate the smallest value from two vectors. */ template @@ -521,7 +561,7 @@ namespace CGLA [](T a, T b){return (std::min)(a,b);}); return v; } - + /** Returns the vector containing for each coordinate the largest value from two vectors. */ template @@ -532,8 +572,8 @@ namespace CGLA [](T a, T b){return (std::max)(a,b);}); return v; } - - + + } - + #endif diff --git a/src/GEL/CGLA/ArithVec3Float.h b/src/GEL/CGLA/ArithVec3Float.h index 022eacc5..f4a66c71 100644 --- a/src/GEL/CGLA/ArithVec3Float.h +++ b/src/GEL/CGLA/ArithVec3Float.h @@ -9,8 +9,8 @@ * @brief Abstract 3D floating point vector class */ -#ifndef CGLA__ARITHVEC3FLOAT_H -#define CGLA__ARITHVEC3FLOAT_H +#ifndef CGLA_ARITHVEC3FLOAT_H +#define CGLA_ARITHVEC3FLOAT_H #include diff --git a/src/GEL/CGLA/ArithVecFloat.h b/src/GEL/CGLA/ArithVecFloat.h index c057fbf9..3e971f0b 100644 --- a/src/GEL/CGLA/ArithVecFloat.h +++ b/src/GEL/CGLA/ArithVecFloat.h @@ -8,8 +8,8 @@ * @brief Abstract 2D floating point vector class */ -#ifndef CGLA__ARITHVECFLOAT_H -#define CGLA__ARITHVECFLOAT_H +#ifndef CGLA_ARITHVECFLOAT_H +#define CGLA_ARITHVECFLOAT_H #include @@ -36,9 +36,10 @@ class ArithVecFloat : public ArithVec { ArithVec(a, b, c, d) {} /// Compute Euclidean length. + [[nodiscard]] constexpr T length() const { - return sqrt(sqr_length(*this)); + return std::sqrt(sqr_length(*this)); } /// Normalize vector. @@ -52,7 +53,7 @@ class ArithVecFloat : public ArithVec { { T sql = sqr_length(*this); if (sql > 0) - (*this) /= sqrt(sql); + (*this) /= std::sqrt(sql); } }; @@ -62,6 +63,7 @@ class ArithVecFloat : public ArithVec { /// Returns length of vector template +[[nodiscard]] constexpr T length(const ArithVecFloat& v) { return v.length(); @@ -70,6 +72,7 @@ constexpr T length(const ArithVecFloat& v) /// Returns normalized vector template +[[nodiscard]] constexpr V normalize(const ArithVecFloat& v) { return v / v.length(); @@ -77,11 +80,12 @@ constexpr V normalize(const ArithVecFloat& v) /// Returns normalized vector if the vector has non-zero length - otherwise the 0 vector. template +[[nodiscard]] constexpr V cond_normalize(const ArithVecFloat& v) { T sql = sqr_length(v); if (sql > 0) - return v / sqrt(sql); + return v / std::sqrt(sql); return v * 1.0; } diff --git a/src/GEL/CGLA/CGLA-util.h b/src/GEL/CGLA/CGLA-util.h index 488bc017..0b349864 100644 --- a/src/GEL/CGLA/CGLA-util.h +++ b/src/GEL/CGLA/CGLA-util.h @@ -11,6 +11,8 @@ #ifndef CGLA_CGLA_UTIL #define CGLA_CGLA_UTIL +#include + #if (_MSC_VER >= 1200) #pragma warning (disable: 4244 4800) #endif @@ -130,20 +132,6 @@ namespace CGLA return result; } - /// Function that seeds the GEL pseudo-random number generator - void gel_srand(unsigned int seed); - - /** GEL provides a linear congruential pseudo-random number - generator which is optimized for speed. This version allows - an integer argument which is useful for grid-based noise - functions. */ - unsigned int gel_rand(unsigned int k); - - /** GEL provides a linear congruential pseudo-random number - generator which is optimized for speed. This means - that GEL_RAND_MAX==UINT_MAX. */ - unsigned int gel_rand(); - /** raw_assign takes a CGLA vector, matrix or whatever has a get() function as its first argument and a raw pointer to a (presumed scalar) entity as the second argument. the contents dereferenced by the pointer is diff --git a/src/GEL/CGLA/Definitions.h b/src/GEL/CGLA/Definitions.h new file mode 100644 index 00000000..d5259901 --- /dev/null +++ b/src/GEL/CGLA/Definitions.h @@ -0,0 +1,99 @@ +// +// Created by Cem Akarsubasi on 8/4/25. +// + +#ifndef CGLA_DEFINITIONS_H +#define CGLA_DEFINITIONS_H + +#include +#include + +namespace CGLA +{ +template +class Vec2; +template +class Vec3; +template +class Vec4; + +template +class Vec2Integral; +template +class Vec3Integral; +template +class Vec4Integral; + +template +class Mat2x2; +template +class Mat3x3; +template +class Mat4x4; +template +class Mat2x3; +template +class Mat3x2; + +using Vec2f = Vec2; +using Vec2d = Vec2; +using Vec3f = Vec3; +using Vec3d = Vec3; +using Vec4f = Vec4; +using Vec4d = Vec4; + +using Vec2c = Vec2Integral; +using Vec2si = Vec2Integral; +using Vec2i = Vec2Integral; +using Vec2l = Vec2Integral; + +using Vec2uc = Vec2Integral; +using Vec2usi = Vec2Integral; +using Vec2ui = Vec2Integral; +using Vec2ul = Vec2Integral; + + +using Vec3c = Vec3Integral; +/// @brief Unsigned short int 3D vector class. +/// +/// This class is mainly useful if we need a 3D int vector that takes up +/// less room than a Vec3i but holds larger numbers than a Vec3c. +using Vec3si = Vec3Integral; +using Vec3i = Vec3Integral; +using Vec3l = Vec3Integral; + +using Vec3uc = Vec3Integral; +using Vec3usi = Vec3Integral; +using Vec3ui = Vec3Integral; +using Vec3ul = Vec3Integral; + +using Vec4c = Vec4Integral; +using Vec4si = Vec4Integral; +using Vec4i = Vec4Integral; +using Vec4l = Vec4Integral; + +using Vec4uc = Vec4Integral; +using Vec4usi = Vec4Integral; +using Vec4ui = Vec4Integral; +using Vec4ul = Vec4Integral; + +using Mat2x2f = Mat2x2; +using Mat2x2d = Mat2x2; + +using Mat3x3f = Mat3x3; +using Mat3x3d = Mat3x3; + +using Mat4x4f = Mat4x4; +using Mat4x4d = Mat4x4; + +using Mat2x3f = Mat2x3; +using Mat2x3d = Mat2x3; + +using Mat3x2f = Mat3x2; +using Mat3x2d = Mat3x2; + +using USInt = std::uint16_t; +using UChar = std::uint8_t; +} + +#endif // CGLA_DEFINITIONS_H diff --git a/src/GEL/CGLA/Mat.h b/src/GEL/CGLA/Mat.h new file mode 100644 index 00000000..463af50e --- /dev/null +++ b/src/GEL/CGLA/Mat.h @@ -0,0 +1,106 @@ +// +// Created by Cem Akarsubasi on 8/4/25. +// + +#ifndef GEL_MAT_H +#define GEL_MAT_H + +#include +#include +#include +#include + +namespace CGLA +{ +/// @brief Two by two double matrix. +/// +/// This class is useful for various +/// vector transformations in the plane. +template +class Mat2x2 final : public ArithSqMat2x2Float, Mat2x2> { +public: + /// Construct a Mat2x2 from two Vec2 vectors. + constexpr Mat2x2(const Vec2& a, const Vec2& b) : ArithSqMat2x2Float, Mat2x2d>(a, b) {} + + /// Construct a Mat2x2 from four scalars. + constexpr Mat2x2(Float a, Float b, Float c, Float d) : + ArithSqMat2x2Float, Mat2x2>(Vec2(a, b), Vec2(c, d)) {} + + /// Construct the NAN matrix + constexpr Mat2x2() = default; + + /// Construct a Mat2x2d from a single scalar + constexpr explicit Mat2x2(Float a) : + ArithSqMat2x2Float, Mat2x2>(a) {} +}; + + +/// @brief 3 by 3 double matrix. +/// +/// This class will typically be used for rotation or +/// scaling matrices for 3D vectors. +template +class Mat3x3 final : public ArithSqMat3x3Float, Mat3x3> { +public: + /// Construct matrix from 3 Vec3d vectors. + constexpr Mat3x3(const Vec3& _a, const Vec3& _b, const Vec3& _c) : + ArithSqMat3x3Float, Mat3x3>(_a, _b, _c) {} + + /// Construct the 0 matrix + constexpr Mat3x3() = default; + + /// Construct a matrix from a single scalar value. + constexpr explicit Mat3x3(Float a) : ArithSqMat3x3Float, Mat3x3>(a) {} +}; + + +/** \brief 4x4 double matrix. + + This class is useful for transformations such as perspective projections + or translation where 3x3 matrices do not suffice. */ +template +class Mat4x4 final : public ArithSqMat4x4Float, Mat4x4> { +public: + /// Construct a Mat4x4d from four Vec4d vectors + constexpr Mat4x4(const Vec4& _a, const Vec4& _b, const Vec4& _c, const Vec4& _d) : + ArithSqMat4x4Float, Mat4x4>(_a, _b, _c, _d) {} + + /// Construct the nan matrix + constexpr Mat4x4() = default; + + /// Construct a matrix with identical elements. + constexpr explicit Mat4x4(Float a) : ArithSqMat4x4Float, Mat4x4>(a) {} +}; + + +/// @brief 2x3 matrix class. +/// +/// This class is useful for projecting a vector from 3D space to 2D. +template +class Mat2x3 final : public ArithMatFloat, Vec3, Mat2x3, 2> { +public: + /// Construct Mat2x3d from two Vec3f vectors (vectors become rows) + constexpr Mat2x3(const Vec3& _a, const Vec3& _b) : + ArithMatFloat, Vec3, Mat2x3, 2>(_a, _b) {} + + /// Construct 0 matrix. + constexpr Mat2x3() = default; +}; + +/// @brief 3x2 double matrix class. +/// +/// This class is useful for going from plane to 3D coordinates. +template +class Mat3x2 final : public ArithMatFloat, Vec2, Mat3x2, 3> { +public: + /** Construct matrix from three Vec2d vectors which become the + rows of the matrix. */ + constexpr Mat3x2(const Vec2& _a, const Vec2& _b, const Vec2& _c) : + ArithMatFloat, Vec2, Mat3x2, 3>(_a, _b, _c) {} + + /// Construct 0 matrix. + constexpr Mat3x2() = default; +}; +} + +#endif //GEL_MAT_H diff --git a/src/GEL/CGLA/Mat2x2d.h b/src/GEL/CGLA/Mat2x2d.h index bb3b6ce5..982b0517 100644 --- a/src/GEL/CGLA/Mat2x2d.h +++ b/src/GEL/CGLA/Mat2x2d.h @@ -11,30 +11,6 @@ #ifndef CGLA_MAT2X2D_H #define CGLA_MAT2X2D_H -#include -#include +#include -namespace CGLA -{ -/** \brief Two by two double matrix. - - This class is useful for various - vector transformations in the plane. */ -class Mat2x2d : public ArithSqMat2x2Float { -public: - /// Construct a Mat2x2d from two Vec2d vectors. - constexpr Mat2x2d(Vec2d a, Vec2d b): ArithSqMat2x2Float(a, b) {} - - /// Construct a Mat2x2f from four scalars. - constexpr Mat2x2d(double a, double b, double c, double d): - ArithSqMat2x2Float(Vec2d(a, b), Vec2d(c, d)) {} - - /// Construct the NAN matrix - constexpr Mat2x2d() = default; - - /// Construct a Mat2x2d from a single scalar - constexpr explicit Mat2x2d(double a): - ArithSqMat2x2Float(a) {} -}; -} #endif diff --git a/src/GEL/CGLA/Mat2x2f.h b/src/GEL/CGLA/Mat2x2f.h index 1768e703..48538512 100644 --- a/src/GEL/CGLA/Mat2x2f.h +++ b/src/GEL/CGLA/Mat2x2f.h @@ -11,30 +11,6 @@ #ifndef CGLA_MAT2X2F_H #define CGLA_MAT2X2F_H -#include -#include +#include - -namespace CGLA -{ -/** \brief Two by two float matrix. - - This class is useful for various - vector transformations in the plane. */ -class Mat2x2f : public ArithSqMat2x2Float { -public: - /// Construct a Mat2x2f from two Vec2f vectors. - constexpr Mat2x2f(Vec2f _a, Vec2f _b): ArithSqMat2x2Float(_a, _b) {} - - /// Construct a Mat2x2f from four scalars. - constexpr Mat2x2f(float _a, float _b, float _c, float _d): - ArithSqMat2x2Float(Vec2f(_a, _b), Vec2f(_c, _d)) {} - - /// Construct the NAN matrix - constexpr Mat2x2f() = default; - - /// Construct a Mat2x2f from a single scalar - constexpr explicit Mat2x2f(float a): ArithSqMat2x2Float(a) {} -}; -} #endif diff --git a/src/GEL/CGLA/Mat2x3d.h b/src/GEL/CGLA/Mat2x3d.h index a69521ea..a3c67c6c 100644 --- a/src/GEL/CGLA/Mat2x3d.h +++ b/src/GEL/CGLA/Mat2x3d.h @@ -11,48 +11,45 @@ #ifndef CGLA_MAT2X3D_H #define CGLA_MAT2X3D_H -#include -#include -#include +#include namespace CGLA { - /** \brief 2x3 double matrix class. - - This class is useful for projecting a vector from 3D space to 2D. - */ - class Mat2x3d: public ArithMatFloat - { - - public: - /// Construct Mat2x3d from two Vec3f vectors (vectors become rows) - constexpr Mat2x3d(const Vec3d& _a, const Vec3d& _b): - ArithMatFloat (_a,_b) {} - - /// Construct 0 matrix. - constexpr Mat2x3d() = default; - }; - - /** \brief 3x2 double matrix class. - - This class is useful for going from plane to 3D coordinates. - */ - class Mat3x2d: public ArithMatFloat - { - - public: - - /** Construct matrix from three Vec2d vectors which become the - rows of the matrix. */ - constexpr Mat3x2d(const Vec2d& _a, const Vec2d& _b, const Vec2d& _c): - ArithMatFloat (_a,_b,_c) {} - - /// Construct 0 matrix. - constexpr Mat3x2d() = default; - - }; - +// /** \brief 2x3 double matrix class. +// +// This class is useful for projecting a vector from 3D space to 2D. +// */ +// class Mat2x3d: public ArithMatFloat +// { +// +// public: +// /// Construct Mat2x3d from two Vec3f vectors (vectors become rows) +// constexpr Mat2x3d(const Vec3d& _a, const Vec3d& _b): +// ArithMatFloat (_a,_b) {} +// +// /// Construct 0 matrix. +// constexpr Mat2x3d() = default; +// }; +// +// /** \brief 3x2 double matrix class. +// +// This class is useful for going from plane to 3D coordinates. +// */ +// class Mat3x2d: public ArithMatFloat +// { +// +// public: +// +// /** Construct matrix from three Vec2d vectors which become the +// rows of the matrix. */ +// constexpr Mat3x2d(const Vec2d& _a, const Vec2d& _b, const Vec2d& _c): +// ArithMatFloat (_a,_b,_c) {} +// +// /// Construct 0 matrix. +// constexpr Mat3x2d() = default; +// +// }; } #endif diff --git a/src/GEL/CGLA/Mat2x3f.h b/src/GEL/CGLA/Mat2x3f.h index 178129e6..6e3aca69 100644 --- a/src/GEL/CGLA/Mat2x3f.h +++ b/src/GEL/CGLA/Mat2x3f.h @@ -17,34 +17,34 @@ namespace CGLA { -/** \brief 2x3 float matrix class. - - This class is useful for projecting a vector from 3D space to 2D. -*/ -class Mat2x3f : public ArithMatFloat { -public: - /// Construct Mat2x3f from two Vec3f vectors (vectors become rows) - constexpr Mat2x3f(const Vec3f& _a, const Vec3f& _b): - ArithMatFloat(_a, _b) {} - - /// Construct NAN matrix. - constexpr Mat2x3f() = default; -}; - - -/** \brief 3x2 float matrix class. - - This class is useful for going from plane to 3D coordinates. -*/ -class Mat3x2f : public ArithMatFloat { -public: - /** Construct matrix from three Vec2f vectors which become the - rows of the matrix. */ - constexpr Mat3x2f(const Vec2f& _a, const Vec2f& _b, const Vec2f& _c): - ArithMatFloat(_a, _b, _c) {} - - /// Construct NAN matrix. - constexpr Mat3x2f() {} -}; +// /** \brief 2x3 float matrix class. +// +// This class is useful for projecting a vector from 3D space to 2D. +// */ +// class Mat2x3f : public ArithMatFloat { +// public: +// /// Construct Mat2x3f from two Vec3f vectors (vectors become rows) +// constexpr Mat2x3f(const Vec3f& _a, const Vec3f& _b): +// ArithMatFloat(_a, _b) {} +// +// /// Construct NAN matrix. +// constexpr Mat2x3f() = default; +// }; +// +// +// /** \brief 3x2 float matrix class. +// +// This class is useful for going from plane to 3D coordinates. +// */ +// class Mat3x2f : public ArithMatFloat { +// public: +// /** Construct matrix from three Vec2f vectors which become the +// rows of the matrix. */ +// constexpr Mat3x2f(const Vec2f& _a, const Vec2f& _b, const Vec2f& _c): +// ArithMatFloat(_a, _b, _c) {} +// +// /// Construct NAN matrix. +// constexpr Mat3x2f() {} +// }; } #endif diff --git a/src/GEL/CGLA/Mat3x3d.h b/src/GEL/CGLA/Mat3x3d.h index e9fa897a..f1e5de2b 100644 --- a/src/GEL/CGLA/Mat3x3d.h +++ b/src/GEL/CGLA/Mat3x3d.h @@ -12,27 +12,26 @@ #define CGLA_MAT3X3D_H #include -#include -#include +#include namespace CGLA { -/** \brief 3 by 3 double matrix. - - This class will typically be used for rotation or - scaling matrices for 3D vectors. */ -class Mat3x3d : public ArithSqMat3x3Float { -public: - /// Construct matrix from 3 Vec3d vectors. - constexpr Mat3x3d(Vec3d _a, Vec3d _b, Vec3d _c): - ArithSqMat3x3Float(_a, _b, _c) {} - - /// Construct the 0 matrix - constexpr Mat3x3d() = default; - - /// Construct a matrix from a single scalar value. - constexpr explicit Mat3x3d(float a): ArithSqMat3x3Float(a) {} -}; +// /** \brief 3 by 3 double matrix. +// +// This class will typically be used for rotation or +// scaling matrices for 3D vectors. */ +// class Mat3x3d : public ArithSqMat3x3Float { +// public: +// /// Construct matrix from 3 Vec3d vectors. +// constexpr Mat3x3d(Vec3d _a, Vec3d _b, Vec3d _c): +// ArithSqMat3x3Float(_a, _b, _c) {} +// +// /// Construct the 0 matrix +// constexpr Mat3x3d() = default; +// +// /// Construct a matrix from a single scalar value. +// constexpr explicit Mat3x3d(float a): ArithSqMat3x3Float(a) {} +// }; /// @name Non-member operations /// @related Mat3x3d diff --git a/src/GEL/CGLA/Mat3x3f.h b/src/GEL/CGLA/Mat3x3f.h index 050eb3a0..b8041934 100644 --- a/src/GEL/CGLA/Mat3x3f.h +++ b/src/GEL/CGLA/Mat3x3f.h @@ -12,27 +12,26 @@ #define CGLA_MAT3X3F_H #include -#include -#include +#include namespace CGLA { -/** \brief 3 by 3 float matrix. - - This class will typically be used for rotation or - scaling matrices for 3D vectors. */ -class Mat3x3f : public ArithSqMat3x3Float { -public: - /// Construct matrix from 3 Vec3f vectors. - constexpr Mat3x3f(Vec3f _a, Vec3f _b, Vec3f _c): - ArithSqMat3x3Float(_a, _b, _c) {} - - /// Construct the 0 matrix - constexpr Mat3x3f() = default; - - /// Construct a matrix from a single scalar value. - constexpr explicit Mat3x3f(float a): ArithSqMat3x3Float(a) {} -}; +// /** \brief 3 by 3 float matrix. +// +// This class will typically be used for rotation or +// scaling matrices for 3D vectors. */ +// class Mat3x3f : public ArithSqMat3x3Float { +// public: +// /// Construct matrix from 3 Vec3f vectors. +// constexpr Mat3x3f(Vec3f _a, Vec3f _b, Vec3f _c): +// ArithSqMat3x3Float(_a, _b, _c) {} +// +// /// Construct the 0 matrix +// constexpr Mat3x3f() = default; +// +// /// Construct a matrix from a single scalar value. +// constexpr explicit Mat3x3f(float a): ArithSqMat3x3Float(a) {} +// }; /// @name Non-member operations /// @related Mat3x3f diff --git a/src/GEL/CGLA/Mat4x4d.h b/src/GEL/CGLA/Mat4x4d.h index f4950a8f..702c37dc 100644 --- a/src/GEL/CGLA/Mat4x4d.h +++ b/src/GEL/CGLA/Mat4x4d.h @@ -16,26 +16,27 @@ #include #include #include +#include namespace CGLA { -/** \brief 4x4 double matrix. - - This class is useful for transformations such as perspective projections - or translation where 3x3 matrices do not suffice. */ -class Mat4x4d : public ArithSqMat4x4Float { -public: - /// Construct a Mat4x4d from four Vec4d vectors - constexpr Mat4x4d(Vec4d _a, Vec4d _b, Vec4d _c, Vec4d _d): - ArithSqMat4x4Float(_a, _b, _c, _d) {} - - /// Construct the nan matrix - constexpr Mat4x4d() = default; - - /// Construct a matrix with identical elements. - constexpr explicit Mat4x4d(double a): ArithSqMat4x4Float(a) {} -}; +// /** \brief 4x4 double matrix. +// +// This class is useful for transformations such as perspective projections +// or translation where 3x3 matrices do not suffice. */ +// class Mat4x4d : public ArithSqMat4x4Float { +// public: +// /// Construct a Mat4x4d from four Vec4d vectors +// constexpr Mat4x4d(Vec4d _a, Vec4d _b, Vec4d _c, Vec4d _d): +// ArithSqMat4x4Float(_a, _b, _c, _d) {} +// +// /// Construct the nan matrix +// constexpr Mat4x4d() = default; +// +// /// Construct a matrix with identical elements. +// constexpr explicit Mat4x4d(double a): ArithSqMat4x4Float(a) {} +// }; /// Create a rotation _matrix. Rotates about one of the major axes. Mat4x4d rotation_Mat4x4d(CGLA::Axis axis, float angle); diff --git a/src/GEL/CGLA/Mat4x4f.h b/src/GEL/CGLA/Mat4x4f.h index a9cdfc9e..2317ed6b 100644 --- a/src/GEL/CGLA/Mat4x4f.h +++ b/src/GEL/CGLA/Mat4x4f.h @@ -13,28 +13,26 @@ #include #include -#include -#include -#include +#include namespace CGLA { -/** \brief 4x4 float matrix. - This class is useful for transformations such as perspective projections - or translation where 3x3 matrices do not suffice. */ -class Mat4x4f : public ArithSqMat4x4Float { -public: - /// Construct a Mat4x4f from four Vec4f vectors - constexpr Mat4x4f(Vec4f _a, Vec4f _b, Vec4f _c, Vec4f _d): - ArithSqMat4x4Float(_a, _b, _c, _d) {} - - /// Construct the NaN matrix - constexpr Mat4x4f() = default; - - /// Construct a matrix with identical elements. - constexpr explicit Mat4x4f(float a) : ArithSqMat4x4Float(a) {} -}; +// /** \brief 4x4 float matrix. +// This class is useful for transformations such as perspective projections +// or translation where 3x3 matrices do not suffice. */ +// class Mat4x4f : public ArithSqMat4x4Float { +// public: +// /// Construct a Mat4x4f from four Vec4f vectors +// constexpr Mat4x4f(Vec4f _a, Vec4f _b, Vec4f _c, Vec4f _d): +// ArithSqMat4x4Float(_a, _b, _c, _d) {} +// +// /// Construct the NaN matrix +// constexpr Mat4x4f() = default; +// +// /// Construct a matrix with identical elements. +// constexpr explicit Mat4x4f(float a) : ArithSqMat4x4Float(a) {} +// }; /// Create a rotation _matrix. Rotates about one of the major axes. Mat4x4f rotation_Mat4x4f(CGLA::Axis axis, float angle); diff --git a/src/GEL/CGLA/Random.cpp b/src/GEL/CGLA/Random.cpp new file mode 100644 index 00000000..7a832539 --- /dev/null +++ b/src/GEL/CGLA/Random.cpp @@ -0,0 +1,36 @@ +/* ----------------------------------------------------------------------- * + * This file is part of GEL, http://www.imm.dtu.dk/GEL + * Copyright (C) the authors and DTU Informatics + * For license and list of authors, see ../../doc/intro.pdf + * ----------------------------------------------------------------------- */ + +#include + +#include +#include + +namespace CGLA::Random +{ +using namespace detail; + +static_assert(std::uniform_random_bit_generator); +static_assert(!std::uniform_random_bit_generator); + +thread_local Pcg32Random pcg32_local = {0x853c49e6748fea9bULL, 0xda3e39cb94b95bdbULL}; + +void gel_srand(unsigned int s) +{ + pcg32_local.pcg32_seed(s, s); +} + +unsigned int gel_rand(unsigned int k) +{ + auto rng = Pcg32Random(0x853c49e6748fea9bULL, k); + return rng.pcg32_rand(); +} + +unsigned int gel_rand() +{ + return pcg32_local.pcg32_rand(); +} +} diff --git a/src/GEL/CGLA/Random.h b/src/GEL/CGLA/Random.h new file mode 100644 index 00000000..12200347 --- /dev/null +++ b/src/GEL/CGLA/Random.h @@ -0,0 +1,369 @@ +// +// Created by Cem Akarsubasi on 8/18/25. +// + +#ifndef GEL_RANDOM_H +#define GEL_RANDOM_H + +#include +#include +#include +#include +#include +#include +#include + +/// Random number generation +namespace CGLA::Random +{ +namespace detail +{ + /// PCG Random Number Generation. Adapted from code by Melissa O'Neill + /// under the Apache 2.0 License. + /// + /// For additional information about the PCG random number generation scheme, + /// including its license and other licensing options, visit + /// + /// http://www.pcg-random.org + struct Pcg32Random { + private: + std::uint64_t state = 0x853c49e6748fea9bULL; + std::uint64_t inc = 0xda3e39cb94b95bdbULL; + + public: + constexpr Pcg32Random() = default; + constexpr Pcg32Random(const std::uint64_t seed, const std::uint64_t seq) { pcg32_seed(seed, seq); } + + /// Seed the rng. Specified in two parts, state initializer and a + /// sequence selection constant (a.k.a. stream id) + constexpr void pcg32_seed(std::uint64_t initstate, std::uint64_t initseq) + { + this->state = 0U; + this->inc = (initseq << 1u) | 1u; + pcg32_rand(); + this->state += initstate; + pcg32_rand(); + } + + /// Generate a uniformly distributed 32-bit random number + constexpr std::uint32_t pcg32_rand() + { + const std::uint64_t oldstate = this->state; + this->state = oldstate * 6364136223846793005ULL + this->inc; + const std::uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u; + const std::uint32_t rot = oldstate >> 59u; + return (xorshifted >> rot) | (xorshifted << ((-rot) & 31)); + } + + /// Generate a uniformly distributed number, r, where 0 <= r < bound + constexpr std::uint32_t pcg32_rand_bounded(std::uint32_t bound) + { + // To avoid bias, we need to make the range of the RNG a multiple of + // bound, which we do by dropping output less than a threshold. + // A naive scheme to calculate the threshold would be to do + // + // uint32_t threshold = 0x100000000ull % bound; + // + // but 64-bit div/mod is slower than 32-bit div/mod (especially on + // 32-bit platforms). In essence, we do + // + // uint32_t threshold = (0x100000000ull-bound) % bound; + // + // because this version will calculate the same modulus, but the LHS + // value is less than 2^32. + + std::uint32_t threshold = -bound % bound; + + // Uniformity guarantees that this loop will terminate. In practice, it + // should usually terminate quickly; on average (assuming all bounds are + // equally likely), 82.25% of the time, we can expect it to require just + // one iteration. In the worst case, someone passes a bound of 2^31 + 1 + // (i.e., 2147483649), which invalidates almost 50% of the range. In + // practice, bounds are typically small and only a tiny amount of the range + // is eliminated. + for (;;) { + std::uint32_t r = pcg32_rand(); + if (r >= threshold) + return r % bound; + } + } + + constexpr std::uint32_t pcg32_rand_bounded_two_sided(std::uint32_t lower, std::uint32_t upper) + { + assert(lower <= upper); + return lower + pcg32_rand_bounded(upper - lower); + } + }; +} + +/// Fast and high-quality PRNG that implements std::uniform_random_bit_generator +template +struct GelPrngBase { + using result_type = std::uint64_t; + static_assert(lower <= upper); + +private: + detail::Pcg32Random rng[2]; + +public: + /// Initialize a random generator with the default seed + constexpr GelPrngBase() + { + pcg32x2_seed(42, 42, 42, 42); + } + + /// Initialize a random generator with the given seed + explicit constexpr GelPrngBase(const std::array& seed) + { + pcg32x2_seed(seed[0], seed[1], seed[2], seed[3]); + } + + [[nodiscard]] + static constexpr result_type min() { return lower; } + + [[nodiscard]] + static constexpr result_type max() { return upper; } + + /// Generate the next random number + [[nodiscard]] + constexpr result_type operator()() + { + if constexpr (lower == 0 && upper == ~0ULL) { + return pcg32x2_rand(); + } else if constexpr ((upper + 1) == 0) { + return lower + pcg32x2_rand_bounded((~0ULL) - lower); + } else { + return pcg32x2_rand_bounded_two_sided(lower, upper + 1); + } + } + + /// Generate the next random number with an external bound + [[nodiscard]] + constexpr result_type operator()(const result_type lower_bound, const result_type upper_bound) + { + return pcg32x2_rand_bounded_two_sided(lower_bound, upper_bound); + } + +private: + constexpr void pcg32x2_seed(std::uint64_t seed1, std::uint64_t seed2, std::uint64_t seq1, std::uint64_t seq2) + { + uint64_t mask = ~0ull >> 1; + // The stream for each of the two generators *must* be distinct + if ((seq1 & mask) == (seq2 & mask)) + seq2 = ~seq2; + rng[0].pcg32_seed(seed1, seq1); + rng[1].pcg32_seed(seed2, seq2); + } + + constexpr std::uint64_t pcg32x2_rand() + { + return (static_cast(rng[0].pcg32_rand()) << 32) + | rng[1].pcg32_rand(); + } + + constexpr std::uint64_t pcg32x2_rand_bounded(std::uint64_t bound) + { + std::uint64_t threshold = -bound % bound; + for (;;) { + std::uint64_t r = pcg32x2_rand(); + if (r >= threshold) + return r % bound; + } + } + + constexpr std::uint64_t pcg32x2_rand_bounded_two_sided(std::uint64_t lower_bound, std::uint64_t upper_bound) + { + assert(lower_bound <= upper_bound); + return lower_bound + pcg32x2_rand_bounded(upper_bound - lower_bound); + } +}; + +namespace Distributions +{ + /// The distribution concept + template + concept Distribution = + std::is_default_constructible_v && std::is_move_constructible_v && requires(GelPrngBase<> rng, T t) + { + typename T::result_type; + { t.min() } -> std::same_as; + { t.max() } -> std::same_as; + { t(rng) } -> std::same_as; + }; + + struct UniformUint { + using result_type = std::uint64_t; + + private: + result_type m_min = 0ULL; + result_type m_max = ~0ULL; + + public: + constexpr UniformUint() = default; + + constexpr UniformUint(const result_type _lower, const result_type _upper) : m_min(_lower), m_max(_upper) + { + if (_lower > _upper) + throw std::invalid_argument("lower bound must be less than upper bound"); + } + + [[nodiscard]] + constexpr result_type min() const { return m_min; } + + [[nodiscard]] + constexpr result_type max() const { return m_max; } + + [[nodiscard]] + constexpr result_type operator()(GelPrngBase<>& rng) const + { + return rng(m_min, m_max); + } + }; + + static_assert(Distribution); + + struct UniformDouble { + using result_type = double; + + [[nodiscard]] + static constexpr result_type min() { return 0.0; } + + [[nodiscard]] + static constexpr result_type max() { return 1.0; } + + [[nodiscard]] + result_type operator()(GelPrngBase<>& rng) const { return std::ldexp(rng(), -64); } + }; + + static_assert(Distribution); + + enum struct NormalGenerator { + BoxMueller, + }; + + template + struct Gaussian; + + template<> + struct Gaussian { + using result_type = double; + constexpr Gaussian() = default; + static constexpr result_type min() { return -std::numeric_limits::infinity(); } + + static constexpr result_type max() + { + return std::numeric_limits::infinity();; + } + + result_type operator()(GelPrngBase<>& rng) const + { + UniformDouble uinf; + const auto x = uinf(rng); + const auto y = uinf(rng); + const auto z = std::sqrt(-2 * std::log(x)) * std::cos(2 * std::numbers::pi * y); + return z; + } + }; + + static_assert(Distribution>); + + template + struct Normal { + using result_type = double; + double mean = 0.0; + double stddev = 1.0; + constexpr Normal() = default; + constexpr Normal(const double mean, const double stddev) : mean(mean), stddev(stddev) {} + static constexpr result_type min() { return -std::numeric_limits::infinity(); } + + static constexpr result_type max() + { + return std::numeric_limits::infinity();; + } + + result_type operator()(GelPrngBase<>& rng) const + { + Gaussian g; + return mean + stddev * g(rng); + } + }; + + static_assert(Distribution>); +} + +/// Core template for all GEL pseudo-random generators +template +// We inherit from Distribution to make use of empty-base optimization. +struct GelPrngWithDistribution : Distribution { + using result_type = Distribution::result_type; + +private: + GelPrngBase<> rng; + +public: + explicit GelPrngWithDistribution() = default; + explicit GelPrngWithDistribution(Distribution&& distribution) : Distribution(std::move(distribution)) {} + explicit GelPrngWithDistribution(const std::array& seed) : rng(seed) {} + + explicit GelPrngWithDistribution(const std::array& seed, + Distribution&& distribution) : Distribution(std::move(distribution)), rng(seed) {} + + [[nodiscard]] + result_type min() const { return Distribution::min(); } + [[nodiscard]] + result_type max() const { return Distribution::max(); } + /// Generate the next random number + result_type operator()() { return Distribution::operator()(rng); } +}; + +/// Uniform double number generator [0.0, 1.0) +using GelPrngDouble = GelPrngWithDistribution; + +/// Uniform uint generator +using GelPrng = GelPrngWithDistribution; + +/// Gaussian double generator +using GelPrngGaussian = GelPrngWithDistribution>; + +/// Normal double generator +using GelPrngNormal = GelPrngWithDistribution>; + +/// Bit generator for std algorithms +using GelBitGenerator = GelPrngBase<>; + +/// Function that seeds the GEL pseudo-random number generator +//[[deprecated]] +void gel_srand(unsigned int seed); + +//// GEL provides a PCG pseudo-random number +/// generator which is optimized for speed. This version allows +/// an integer argument which is useful for grid-based noise +/// functions. +unsigned int gel_rand(unsigned int k); + +/// GEL provides a PCG pseudo-random number +/// generator which is optimized for speed. This means +/// that GEL_RAND_MAX==UINT_MAX. +//[[deprecated]] +unsigned int gel_rand(); +} + +namespace CGLA +{ +// Reexports + +using UniformIntegerDistribution = Random::Distributions::UniformUint; +using UniformDoubleDistribution = Random::Distributions::UniformDouble; +using GaussianDistribution = Random::Distributions::Gaussian<>; +using NormalDistribution = Random::Distributions::Normal<>; +using Random::GelPrng; +using Random::GelPrngDouble; +using Random::GelPrngGaussian; +using Random::GelPrngNormal; +using Random::GelPrngWithDistribution; +using Random::GelBitGenerator; +using Random::gel_rand; +using Random::gel_srand; +} + +#endif //GEL_RANDOM_H diff --git a/src/GEL/CGLA/Vec.h b/src/GEL/CGLA/Vec.h new file mode 100644 index 00000000..ff81dd57 --- /dev/null +++ b/src/GEL/CGLA/Vec.h @@ -0,0 +1,216 @@ +// +// Created by Cem Akarsubasi on 8/4/25. +// + +#ifndef CGLA_VEC_H +#define CGLA_VEC_H + +#include +#include +#include +#include +#include +#include + +namespace CGLA +{ +template +class Vec2 final : public ArithVec2Float> { +public: + constexpr Vec2() = default; + + constexpr Vec2(Float _a, Float _b): + ArithVec2Float(_a, _b) {} + + template + constexpr explicit Vec2(const Vec2& v): + ArithVec2Float(v[0], v[1]) {} + + constexpr explicit Vec2(Float a): + ArithVec2Float(a, a) {} + + template + constexpr explicit Vec2(const ArithVec& v): + ArithVec2Float(static_cast(v[0]), + static_cast(v[1])) {} +}; + +template +class Vec2Integral final : public ArithVec, 2> { +public: + /// Construct 0 vector + constexpr Vec2Integral() = default; + + /// Construct 2D int vector + constexpr Vec2Integral(Integral _a, Integral _b): ArithVec(_a, _b) {} + + /// Construct a 2D integer vector with two identical coordinates. + constexpr explicit Vec2Integral(Integral a): ArithVec(a, a) {} + + /// Convert from 2D float vector + template + constexpr explicit Vec2Integral(const Vec2& v) : + ArithVec(static_cast(floor(v[0])), static_cast(floor(v[1]))) {} +}; + + +/** \brief A 3 dimensional vector. */ +template +class Vec3 final : public ArithVec3Float> { +public: + /// Construct 0 vector + constexpr Vec3() = default; + + /// Construct vector + constexpr Vec3(Float a, Float b, Float c): ArithVec3Float(a, b, c) {} + + /// Construct vector where all coords = a + constexpr explicit Vec3(Float a): + ArithVec3Float(a, a, a) {} + + /// Convert from int vector + template + constexpr explicit Vec3(const Vec3Integral& v): + ArithVec3Float(v[0], v[1], v[2]) {} + + /// Convert from float vector + template + constexpr explicit Vec3(const Vec3& v): + ArithVec3Float(v[0], v[1], v[2]) {} + + /// Construct from a 4D float vector (skipping the last value) + template + constexpr explicit Vec3(const Vec4& v): ArithVec3Float(v[0], v[1], v[2]) {} +}; + +/** \brief 3D integer vector. + + This class does not really extend the template + and hence provides only the basic facilities of an ArithVec. + The class is typically used for indices to 3D voxel grids. */ +template +class Vec3Integral final : public ArithVec3Int> { +public: + /// Construct 0 vector. + constexpr Vec3Integral() = default; + + /// Construct a 3D integer vector. + constexpr Vec3Integral(Integral _a, Integral _b, Integral _c): ArithVec3Int(_a, _b, _c) {} + + /// Construct a 3D integer vector with 3 identical coordinates. + constexpr explicit Vec3Integral(Integral a): ArithVec3Int(a, a, a) {} + + /// Construct from a Vec3. + template + explicit Vec3Integral(const Vec3& v): ArithVec3Int(Integral(std::floor(v[0])), + Integral(std::floor(v[1])), + Integral(std::floor(v[2]))) {} + + /// Construct from Vec3Integral with a smaller size. + template requires (sizeof(Integral) >= sizeof(OtherIntegral)) + explicit Vec3Integral(const Vec3Integral& v) : ArithVec3Int(v[0], v[1], v[2]) + {} +}; + +/** \brief A four dimensional floating point vector. + + This class is also used (via typedef) for + homogeneous vectors. +*/ +template +class Vec4 final : public ArithVec4Float> { +public: + /// Construct a (0,0,0,0) homogenous Vector + constexpr Vec4(): ArithVec4Float(0.0, 0.0, 0.0, 0.0) {} + + /// Construct a (0,0,0,0) homogenous Vector + constexpr explicit Vec4(Float _a): ArithVec4Float(_a, _a, _a, _a) {} + + /// Construct a 4D vector + constexpr Vec4(Float _a, Float _b, Float _c, Float _d): + ArithVec4Float(_a, _b, _c, _d) {} + + /// Construct a homogenous vector from a non-homogenous. + constexpr explicit Vec4(const Vec3& v, Float _d): + ArithVec4Float(v[0], v[1], v[2], _d) {} +}; + +// 4D Integral vector. +// +// This class does not really extend the template +// and hence provides only the basic facilities of an ArithVec. +// The class is typically used for indices to 4D voxel grids. +template +class Vec4Integral final: public ArithVec4Int> { +public: + /// Construct 0 vector. + constexpr Vec4Integral() = default; + + /// Construct a 4D integer vector. + constexpr Vec4Integral(int _a, int _b, int _c, int _d): ArithVec4Int(_a, _b, _c, _d) {} + + /// Construct a 4D integer vector with 4 identical coordinates. + constexpr explicit Vec4Integral(int a): ArithVec4Int(a, a, a, a) {} + + /// Construct from a floating point vector. + template + explicit Vec4Integral(const Vec4& v) : ArithVec4Int(Integral(std::floor(v[0])), + Integral(std::floor(v[1])), + Integral(std::floor(v[2])), Integral(std::floor(v[3]))) {} + + /// Construct from an integral vector. + template + explicit Vec4Integral(const Vec4Integral& v) : ArithVec4Int( + v[0], v[1], v[2], v[3]) {} +}; + +template +auto any(const VecOrMat& v, Predicate&& p) -> bool +{ + for (const auto& elem: v) { + if (p(elem)) { + return true; + } + } + return false; +} + +template +auto all(const VecOrMat& v, Predicate&& p) -> bool +{ + for (const auto& elem: v) { + if (!p(v)) { + return false; + } + } + return true; +} + +template <> +struct VecT_to_MatT { + using MatT = Mat2x2d; +}; + +template <> +struct VecT_to_MatT { + using MatT = Mat4x4d; +}; + +template <> +struct VecT_to_MatT { + using MatT = Mat4x4f; +}; + +template <> +struct VecT_to_MatT { + using MatT = Mat3x3d; +}; + +template <> +struct VecT_to_MatT { + using MatT = Mat3x3f; +}; +} + + +#endif // CGLA_VEC_H diff --git a/src/GEL/CGLA/Vec2d.h b/src/GEL/CGLA/Vec2d.h index c3e9c019..0cb303ee 100644 --- a/src/GEL/CGLA/Vec2d.h +++ b/src/GEL/CGLA/Vec2d.h @@ -11,37 +11,6 @@ #ifndef CGLA_VEC2D_H #define CGLA_VEC2D_H -#include -#include -#include +#include - -namespace CGLA -{ - -/// @brief 2D double floating point vector -class Vec2d : public ArithVec2Float { -public: - constexpr Vec2d() = default; - - constexpr Vec2d(double _a, double _b): - ArithVec2Float(_a, _b) {} - - constexpr explicit Vec2d(const Vec2i& v): - ArithVec2Float(v[0], v[1]) {} - - constexpr explicit Vec2d(const Vec2f& v): - ArithVec2Float(v[0], v[1]) {} - - constexpr explicit Vec2d(double a): - ArithVec2Float(a, a) {} -}; - -class Mat2x2d; - -template <> -struct VecT_to_MatT { - using MatT = Mat2x2d; -}; -} #endif diff --git a/src/GEL/CGLA/Vec2f.h b/src/GEL/CGLA/Vec2f.h index 2de08ba3..7516d764 100644 --- a/src/GEL/CGLA/Vec2f.h +++ b/src/GEL/CGLA/Vec2f.h @@ -11,30 +11,6 @@ #ifndef CGLA_VEC2F_H #define CGLA_VEC2F_H -#include +#include -namespace CGLA -{ - -/// @brief 2D floating point vector -class Vec2f : public ArithVec2Float { -public: - constexpr Vec2f() = default; - constexpr Vec2f(float _a, float _b): ArithVec2Float(_a, _b) {} - - template - constexpr explicit Vec2f(const ArithVec& v): - ArithVec2Float(static_cast(v[0]), - static_cast(v[1])) {} - - constexpr explicit Vec2f(float a): ArithVec2Float(a, a) {} -}; - -class Mat2x2f; - -template <> -struct VecT_to_MatT { - using MatT = Mat2x2f; -}; -} #endif diff --git a/src/GEL/CGLA/Vec2i.cpp b/src/GEL/CGLA/Vec2i.cpp deleted file mode 100644 index 6fa7cedb..00000000 --- a/src/GEL/CGLA/Vec2i.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/* ----------------------------------------------------------------------- * - * This file is part of GEL, http://www.imm.dtu.dk/GEL - * Copyright (C) the authors and DTU Informatics - * For license and list of authors, see ../../doc/intro.pdf - * ----------------------------------------------------------------------- */ - -#include -#include - -namespace CGLA { - - Vec2i::Vec2i(const Vec2f& v): - ArithVec(int(floor(v[0])), int(floor(v[1]))) {} - -} diff --git a/src/GEL/CGLA/Vec2i.h b/src/GEL/CGLA/Vec2i.h index 9033804c..681e1201 100644 --- a/src/GEL/CGLA/Vec2i.h +++ b/src/GEL/CGLA/Vec2i.h @@ -11,26 +11,6 @@ #ifndef CGLA_VEC2I_H #define CGLA_VEC2I_H -#include +#include -namespace CGLA -{ -class Vec2f; - -/// @brief 2D Integer vector. -class Vec2i : public ArithVec { -public: - /// Construct 0 vector - constexpr Vec2i() = default; - - /// Construct 2D int vector - constexpr Vec2i(int _a, int _b): ArithVec(_a, _b) {} - - /// Construct a 2D integer vector with two identical coordinates. - constexpr explicit Vec2i(int a): ArithVec(a, a) {} - - /// Convert from 2D float vector - explicit Vec2i(const Vec2f& v); -}; -} #endif diff --git a/src/GEL/CGLA/Vec2ui.h b/src/GEL/CGLA/Vec2ui.h index 28d3a706..a57e87ef 100644 --- a/src/GEL/CGLA/Vec2ui.h +++ b/src/GEL/CGLA/Vec2ui.h @@ -12,34 +12,6 @@ #ifndef CGLA_VEC2UI_H #define CGLA_VEC2UI_H -#include +#include -namespace CGLA -{ - class Vec2f; - - /** \brief 2D Integer vector. */ - - class Vec2ui: public ArithVec - { - public: - - /// Construct 0 vector - constexpr Vec2ui() = default; - - /// Construct 2D int vector - constexpr Vec2ui(unsigned int _a) - : ArithVec(_a,_a) - {} - - /// Construct 2D int vector - constexpr Vec2ui(unsigned int _a, unsigned int _b) - : ArithVec(_a,_b) - {} - - /// Convert from 2D float vector - constexpr explicit Vec2ui(const Vec2f& v); - - }; -} #endif diff --git a/src/GEL/CGLA/Vec3d.h b/src/GEL/CGLA/Vec3d.h index 94489fc5..bde29cac 100644 --- a/src/GEL/CGLA/Vec3d.h +++ b/src/GEL/CGLA/Vec3d.h @@ -11,46 +11,6 @@ #ifndef CGLA_VEC3D_H #define CGLA_VEC3D_H -#include -#include -#include -#include +#include - -namespace CGLA -{ -/** \brief A 3D double vector. -Useful for high precision arithmetic. */ -class Vec3d : public ArithVec3Float { -public: - /// Construct 0 vector - constexpr Vec3d() = default; - - /// Construct vector - constexpr Vec3d(double a, double b, double c): ArithVec3Float(a, b, c) {} - - /// Construct vector where all coords = a - constexpr explicit Vec3d(double a): - ArithVec3Float(a, a, a) {} - - /// Convert from int vector - constexpr explicit Vec3d(const Vec3i& v): - ArithVec3Float(v[0], v[1], v[2]) {} - - /// Construct from a 3D unsigned int vector. - constexpr explicit Vec3d(const Vec3usi& v): - ArithVec3Float(v[0], v[1], v[2]) {} - - /// Convert from float vector - constexpr explicit Vec3d(const Vec3f& v): - ArithVec3Float(v[0], v[1], v[2]) {} -}; - -class Mat3x3d; - -template <> -struct VecT_to_MatT { - using MatT = Mat3x3d; -}; -} #endif diff --git a/src/GEL/CGLA/Vec3f.cpp b/src/GEL/CGLA/Vec3f.cpp deleted file mode 100644 index 234005d0..00000000 --- a/src/GEL/CGLA/Vec3f.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* ----------------------------------------------------------------------- * - * This file is part of GEL, http://www.imm.dtu.dk/GEL - * Copyright (C) the authors and DTU Informatics - * For license and list of authors, see ../../doc/intro.pdf - * ----------------------------------------------------------------------- */ - -#include - -#include -#include -#include - -using namespace std; - -namespace CGLA -{ -Vec3f::Vec3f(const Vec3d& v): - ArithVec3Float(static_cast(v[0]), - static_cast(v[1]), - static_cast(v[2])) {} - -Vec3f::Vec3f(const Vec4f& v): - ArithVec3Float(v[0], v[1], v[2]) {} -} diff --git a/src/GEL/CGLA/Vec3f.h b/src/GEL/CGLA/Vec3f.h index c2987f77..5dd842f5 100644 --- a/src/GEL/CGLA/Vec3f.h +++ b/src/GEL/CGLA/Vec3f.h @@ -11,56 +11,6 @@ #ifndef CGLA_VEC3F_H #define CGLA_VEC3F_H -#include -#include -#include +#include -namespace CGLA -{ -class Vec3d; -class Vec4f; - -/** \brief 3D float vector. - - Class Vec3f is the vector typically used in 3D computer graphics. - The class has many constructors since we may need to convert from - other vector types. Most of these are explicit to avoid automatic - conversion. -*/ -class Vec3f : public ArithVec3Float { -public: - /// Construct 0 vector. - constexpr Vec3f() = default; - - /// Construct a 3D float vector. - constexpr Vec3f(float a, float b, float c): - ArithVec3Float(a, b, c) {} - - /// Construct a vector with 3 identical coordinates. - constexpr explicit Vec3f(float a): - ArithVec3Float(a, a, a) {} - - /// Construct from a 3D int vector - constexpr explicit Vec3f(const Vec3i& v): - ArithVec3Float(v[0], v[1], v[2]) {} - - /// Construct from a 3D unsigned int vector. - constexpr explicit Vec3f(const Vec3usi& v): - ArithVec3Float(v[0], v[1], v[2]) {} - - /// Construct from a 3D double vector. - explicit Vec3f(const Vec3d&); - - /// Construct from a 4D float vector (skipping the last value) - explicit Vec3f(const Vec4f&); -}; - - -class Mat3x3f; - -template <> -struct VecT_to_MatT { - using MatT = Mat3x3f; -}; -} #endif diff --git a/src/GEL/CGLA/Vec3i.cpp b/src/GEL/CGLA/Vec3i.cpp index 32b37ac5..c0d2bc4d 100644 --- a/src/GEL/CGLA/Vec3i.cpp +++ b/src/GEL/CGLA/Vec3i.cpp @@ -10,20 +10,4 @@ #include namespace CGLA { - - Vec3i::Vec3i(const Vec3f& v): - ArithVec3Int(int(floor(v[0])), - int(floor(v[1])), - int(floor(v[2]))) {} - - Vec3i::Vec3i(const Vec3d& v): - ArithVec3Int(int(floor(v[0])), - int(floor(v[1])), - int(floor(v[2]))) {} - - Vec3i::Vec3i(const Vec3uc& v): - ArithVec3Int(v[0],v[1],v[2]) {} - - Vec3i::Vec3i(const Vec3usi& v): - ArithVec3Int(v[0],v[1],v[2]) {} } diff --git a/src/GEL/CGLA/Vec3i.h b/src/GEL/CGLA/Vec3i.h index 2c1d1897..ce78a368 100644 --- a/src/GEL/CGLA/Vec3i.h +++ b/src/GEL/CGLA/Vec3i.h @@ -12,42 +12,40 @@ #ifndef CGLA_VEC3I_H #define CGLA_VEC3I_H -#include +#include namespace CGLA { -class Vec3f; -class Vec3d; -class Vec3uc; -class Vec3usi; - -/** \brief 3D integer vector. - - This class does not really extend the template - and hence provides only the basic facilities of an ArithVec. - The class is typically used for indices to 3D voxel grids. */ -class Vec3i : public ArithVec3Int { -public: - /// Construct 0 vector. - constexpr Vec3i() = default; - - /// Construct a 3D integer vector. - constexpr Vec3i(int _a, int _b, int _c): ArithVec3Int(_a, _b, _c) {} - - /// Construct a 3D integer vector with 3 identical coordinates. - constexpr explicit Vec3i(int a): ArithVec3Int(a, a, a) {} - - /// Construct from a Vec3f. - explicit Vec3i(const Vec3f& v); - - /// Construct from a Vec3f. - explicit Vec3i(const Vec3d& v); - - /// Construct from a Vec3uc. - explicit Vec3i(const Vec3uc& v); - - /// Construct from a Vec3usi. - explicit Vec3i(const Vec3usi& v); -}; +// class Vec3uc; +// class Vec3usi; +// +// /** \brief 3D integer vector. +// +// This class does not really extend the template +// and hence provides only the basic facilities of an ArithVec. +// The class is typically used for indices to 3D voxel grids. */ +// class Vec3i : public ArithVec3Int { +// public: +// /// Construct 0 vector. +// constexpr Vec3i() = default; +// +// /// Construct a 3D integer vector. +// constexpr Vec3i(int _a, int _b, int _c): ArithVec3Int(_a, _b, _c) {} +// +// /// Construct a 3D integer vector with 3 identical coordinates. +// constexpr explicit Vec3i(int a): ArithVec3Int(a, a, a) {} +// +// /// Construct from a Vec3f. +// explicit Vec3i(const Vec3f& v); +// +// /// Construct from a Vec3f. +// explicit Vec3i(const Vec3d& v); +// +// /// Construct from a Vec3uc. +// explicit Vec3i(const Vec3uc& v); +// +// /// Construct from a Vec3usi. +// explicit Vec3i(const Vec3usi& v); +// }; } #endif diff --git a/src/GEL/CGLA/Vec3uc.h b/src/GEL/CGLA/Vec3uc.h index 9090aaca..56229a4a 100644 --- a/src/GEL/CGLA/Vec3uc.h +++ b/src/GEL/CGLA/Vec3uc.h @@ -11,25 +11,26 @@ #ifndef CGLA_VEC3UC_H #define CGLA_VEC3UC_H -#include +#include -namespace CGLA -{ -typedef unsigned char UChar; - -/// @brief 3D unsigned char vector. -class Vec3uc : public ArithVec3Int { -public: - /// Construct 0 vector - constexpr Vec3uc() = default; - - /// Construct 3D uchar vector - constexpr Vec3uc(UChar a, UChar b, UChar c): - ArithVec3Int(a, b, c) {} - - /// Convert from int vector. - constexpr explicit Vec3uc(const Vec3i& v): - ArithVec3Int(v[0] & 0xff, v[1] & 0xff, v[2] & 0xff) {} -}; -} +// namespace CGLA +// { +// typedef unsigned char UChar; +// +// /// @brief 3D unsigned char vector. +// class Vec3uc : public ArithVec3Int { +// public: +// /// Construct 0 vector +// constexpr Vec3uc() = default; +// +// /// Construct 3D uchar vector +// constexpr Vec3uc(UChar a, UChar b, UChar c): +// ArithVec3Int(a, b, c) {} +// +// /// Convert from int vector. +// TODO: +// constexpr explicit Vec3uc(const Vec3i& v): +// ArithVec3Int(v[0] & 0xff, v[1] & 0xff, v[2] & 0xff) {} +// }; +// } #endif diff --git a/src/GEL/CGLA/Vec3usi.h b/src/GEL/CGLA/Vec3usi.h index bb061622..cae9fae5 100644 --- a/src/GEL/CGLA/Vec3usi.h +++ b/src/GEL/CGLA/Vec3usi.h @@ -11,28 +11,28 @@ #ifndef CGLA_VEC3USI_H #define CGLA_VEC3USI_H -#include +#include namespace CGLA { -typedef unsigned short int USInt; -/** \brief Unsigned short int 3D vector class. - This class is mainly useful if we need a 3D int vector that takes up - less room than a Vec3i but holds larger numbers than a Vec3c. */ -class Vec3usi : public ArithVec3Int { -public: - /// Construct 0 vector. - constexpr Vec3usi() = default; - - /// Construct a Vec3usi - constexpr Vec3usi(USInt _a, USInt _b, USInt _c): - ArithVec3Int(_a, _b, _c) {} - - /// Construct a Vec3usi from a Vec3i. - constexpr explicit Vec3usi(const Vec3i& v): - ArithVec3Int(v[0] & 0xffff, v[1] & 0xffff, v[2] & 0xffff) {} -}; +// /** \brief Unsigned short int 3D vector class. +// +// This class is mainly useful if we need a 3D int vector that takes up +// less room than a Vec3i but holds larger numbers than a Vec3c. */ +// class Vec3usi : public ArithVec3Int { +// public: +// /// Construct 0 vector. +// constexpr Vec3usi() = default; +// +// /// Construct a Vec3usi +// constexpr Vec3usi(USInt _a, USInt _b, USInt _c): +// ArithVec3Int(_a, _b, _c) {} +// +// /// Construct a Vec3usi from a Vec3i. +// constexpr explicit Vec3usi(const Vec3i& v): +// ArithVec3Int(v[0] & 0xffff, v[1] & 0xffff, v[2] & 0xffff) {} +// }; } #endif diff --git a/src/GEL/CGLA/Vec4d.h b/src/GEL/CGLA/Vec4d.h index 260b7e0a..090f0591 100644 --- a/src/GEL/CGLA/Vec4d.h +++ b/src/GEL/CGLA/Vec4d.h @@ -11,38 +11,6 @@ #ifndef CGLA_VEC4D_H #define CGLA_VEC4D_H -#include -#include +#include -namespace CGLA -{ -/** \brief A four dimensional floating point vector. - - This class is also used (via typedef) for - homogeneous vectors. -*/ -class Vec4d : public ArithVec4Float { -public: - /// Construct a (0,0,0,0) homogenous Vector - constexpr Vec4d(): ArithVec4Float(0.0, 0.0, 0.0, 0.0) {} - - /// Construct a (0,0,0,0) homogenous Vector - constexpr explicit Vec4d(double _a): ArithVec4Float(_a, _a, _a, _a) {} - - /// Construct a 4D vector - constexpr Vec4d(double _a, double _b, double _c, double _d): - ArithVec4Float(_a, _b, _c, _d) {} - - /// Construct a homogenous vector from a non-homogenous. - constexpr explicit Vec4d(const Vec3d& v, double _d): - ArithVec4Float(v[0], v[1], v[2], _d) {} -}; - -class Mat4x4d; - -template <> -struct VecT_to_MatT { - using MatT = Mat4x4d; -}; -} #endif diff --git a/src/GEL/CGLA/Vec4f.h b/src/GEL/CGLA/Vec4f.h index e65fadcd..482212d7 100644 --- a/src/GEL/CGLA/Vec4f.h +++ b/src/GEL/CGLA/Vec4f.h @@ -11,38 +11,6 @@ #ifndef CGLA_VEC4F_H #define CGLA_VEC4F_H -#include -#include +#include -namespace CGLA -{ -/** \brief A four dimensional floating point vector. - - This class is also used (via typedef) for - homogeneous vectors. -*/ -class Vec4f : public ArithVec4Float { -public: - /// Construct a (0,0,0,0) homogenous Vector - constexpr Vec4f(): ArithVec4Float(0.0f, 0.0f, 0.0f, 0.0f) {} - - /// Construct a (0,0,0,0) homogenous Vector - constexpr explicit Vec4f(float _a): ArithVec4Float(_a, _a, _a, _a) {} - - /// Construct a 4D vector - constexpr Vec4f(float _a, float _b, float _c, float _d): - ArithVec4Float(_a, _b, _c, _d) {} - - /// Construct a homogenous vector from a non-homogenous. - constexpr explicit Vec4f(const Vec3f& v, float _d): - ArithVec4Float(v[0], v[1], v[2], _d) {} -}; - -class Mat4x4f; - -template <> -struct VecT_to_MatT { - using MatT = Mat4x4f; -}; -} #endif diff --git a/src/GEL/CGLA/Vec4i.h b/src/GEL/CGLA/Vec4i.h index 13a145ba..86785e90 100644 --- a/src/GEL/CGLA/Vec4i.h +++ b/src/GEL/CGLA/Vec4i.h @@ -11,43 +11,6 @@ #ifndef CGLA_VEC4I_H #define CGLA_VEC4I_H -#include +#include -namespace CGLA -{ - class Vec4f; - class Vec4uc; - class Vec4usi; - - /** \brief 4D integer vector. - - This class does not really extend the template - and hence provides only the basic facilities of an ArithVec. - The class is typically used for indices to 4D voxel grids. */ - class Vec4i: public ArithVec4Int - { - public: - - /// Construct 0 vector. - constexpr Vec4i() = default; - - /// Construct a 4D integer vector. - constexpr Vec4i(int _a,int _b,int _c, int _d): ArithVec4Int(_a,_b,_c,_d) {} - - /// Construct a 4D integer vector with 4 identical coordinates. - constexpr explicit Vec4i(int a): ArithVec4Int(a,a,a,a) {} - - /// Construct from a Vec4f. - explicit Vec4i(const Vec4f& v); - - /// Construct from a Vec4uc. - explicit Vec4i(const Vec4uc& v); - - /// Construct from a Vec4usi. - explicit Vec4i(const Vec4usi& v); - - }; - - -} #endif diff --git a/src/GEL/CGLA/Vec4uc.h b/src/GEL/CGLA/Vec4uc.h index 932c4d00..33ae08bb 100644 --- a/src/GEL/CGLA/Vec4uc.h +++ b/src/GEL/CGLA/Vec4uc.h @@ -12,38 +12,38 @@ #ifndef CGLA_VEC4UC_H #define CGLA_VEC4UC_H -#include +#include namespace CGLA { - typedef unsigned char UChar; - - /** \brief 4D unsigned char vector. */ - class Vec4uc: public ArithVec - { - - public: - - /// Construct 0 vector - constexpr Vec4uc() = default; - - /// Construct a 4D unsigned char vector with 4 identical coordinates. - constexpr Vec4uc(unsigned char a): ArithVec(a,a,a,a) {} - - /// Construct 4D uchar vector - Vec4uc(UChar _a, UChar _b, UChar _c,UChar _d): - ArithVec(_a,_b,_c,_d) {} - - /// Convert from float vector. - explicit Vec4uc(const Vec4f& v): - ArithVec(UChar(v[0]), UChar(v[1]), - UChar(v[2]), UChar(v[3])) {} - - operator Vec4f() const - { - return Vec4f((*this)[0],(*this)[1],(*this)[2],(*this)[3]); - } - - }; + // typedef unsigned char UChar; + // + // /** \brief 4D unsigned char vector. */ + // class Vec4uc: public ArithVec + // { + // + // public: + // + // /// Construct 0 vector + // constexpr Vec4uc() = default; + // + // /// Construct a 4D unsigned char vector with 4 identical coordinates. + // constexpr Vec4uc(unsigned char a): ArithVec(a,a,a,a) {} + // + // /// Construct 4D uchar vector + // Vec4uc(UChar _a, UChar _b, UChar _c,UChar _d): + // ArithVec(_a,_b,_c,_d) {} + // + // /// Convert from float vector. + // explicit Vec4uc(const Vec4f& v): + // ArithVec(UChar(v[0]), UChar(v[1]), + // UChar(v[2]), UChar(v[3])) {} + // + // explicit operator Vec4f() const + // { + // return Vec4f((*this)[0],(*this)[1],(*this)[2],(*this)[3]); + // } + // + // }; } diff --git a/src/GEL/CGLA/gel_rand.cpp b/src/GEL/CGLA/gel_rand.cpp deleted file mode 100644 index 864de43c..00000000 --- a/src/GEL/CGLA/gel_rand.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* ----------------------------------------------------------------------- * - * This file is part of GEL, http://www.imm.dtu.dk/GEL - * Copyright (C) the authors and DTU Informatics - * For license and list of authors, see ../../doc/intro.pdf - * ----------------------------------------------------------------------- */ - -#include - -namespace -{ -thread_local unsigned int seed = 1; -thread_local unsigned int current_rand = 1; -} - -namespace CGLA -{ -void gel_srand(unsigned int s) -{ - seed = current_rand = s; -} - -unsigned int gel_rand(unsigned int k) -{ - unsigned int b = 3125; - unsigned int c = 49; - unsigned int result = seed; - - for (; k > 0; k >>= 1) { - if (k & 1) result = result * b + c; - c += b * c; - b *= b; - } - return result; -} - -unsigned int gel_rand() -{ - const unsigned int b = 3125; - const unsigned int c = 49; - - current_rand = current_rand * b + c; - return current_rand; -} -} diff --git a/src/GEL/Geometry/Graph.cpp b/src/GEL/Geometry/Graph.cpp index 5b0e55ab..89c8fa8a 100644 --- a/src/GEL/Geometry/Graph.cpp +++ b/src/GEL/Geometry/Graph.cpp @@ -67,13 +67,6 @@ namespace Geometry { pos[n_new] /= ns.size(); return n_new; } - - - /// Special ID value for invalid node - const AMGraph3D::NodeID AMGraph::InvalidNodeID = std::numeric_limits::max(); - - /// Special ID value for invalid edge - const AMGraph3D::EdgeID AMGraph::InvalidEdgeID = std::numeric_limits::max(); AMGraph3D clean_graph(const AMGraph3D& g) { diff --git a/src/GEL/Geometry/Graph.h b/src/GEL/Geometry/Graph.h index 25dd42df..8ba09628 100644 --- a/src/GEL/Geometry/Graph.h +++ b/src/GEL/Geometry/Graph.h @@ -6,18 +6,16 @@ // Copyright © 2016 J. Andreas Bærentzen. All rights reserved. // -#ifndef Graph_h -#define Graph_h +#ifndef GEOMETRY_GRAPH_H +#define GEOMETRY_GRAPH_H #include -#include -#include -#include #include #include #include #include #include +#include namespace Geometry { @@ -35,19 +33,20 @@ namespace Geometry { using NodeID = size_t; /// Node Set type - using NodeSet = std::set; + // FIXME: Ensure we don't rely on ordering and replace this with Util::HashSet + using NodeSet = Util::BTreeSet; /// ID type for edges using EdgeID = size_t; /// The adjacency map class - using AdjMap = std::map; - + using AdjMap = Util::HashMap; + /// Special ID value for invalid node - static const NodeID InvalidNodeID;// = std::numeric_limits::max(); - + static constexpr NodeID InvalidNodeID = std::numeric_limits::max(); + /// Special ID value for invalid edge - static const EdgeID InvalidEdgeID;// = std::numeric_limits::max(); + static constexpr EdgeID InvalidEdgeID = std::numeric_limits::max(); protected: @@ -91,25 +90,25 @@ namespace Geometry { /// Add a node to the graph NodeID add_node() { - NodeID id = edge_map.size(); - edge_map.push_back(AdjMap()); + const NodeID id = edge_map.size(); + edge_map.emplace_back(); return id; } /// Find an edge in the graph given two nodes. Returns InvalidEdgeID if no such edge found. - EdgeID find_edge(NodeID n0, NodeID n1) const + [[nodiscard]] + EdgeID find_edge(const NodeID n0, const NodeID n1) const { if(valid_node_id(n0) && valid_node_id(n1)) { - auto it = edge_map[n0].find(n1); - if (it != edge_map[n0].end()) + if (const auto it = edge_map[n0].find(n1); it != edge_map[n0].end()) return it->second; } return InvalidEdgeID; } - /** Connect two nodes. Returns the id of the created edge or InvalidEdgeID if either the nodes - were not valid or the edge already existed. */ - EdgeID connect_nodes(NodeID n0, NodeID n1) + /// Connect two nodes. Returns the id of the existing or created edge. Returns InvalidEdgeID if either of + /// the nodes were not valid. + EdgeID connect_nodes(const NodeID n0, const NodeID n1) { if(valid_node_id(n0) && valid_node_id(n1)) { size_t id = find_edge(n0, n1); @@ -122,32 +121,66 @@ namespace Geometry { } return InvalidEdgeID; } - + + /// Return the NodeIDs of nodes adjacent to a given node lazily + [[nodiscard]] std::ranges::input_range + auto neighbors_lazy(const NodeID n) const + { + GEL_ASSERT(valid_node_id(n)); + return edge_map[n] | std::views::keys; + } + /// Return the NodeIDs of nodes adjacent to a given node - std::vector neighbors(NodeID n) const { - std::vector nbrs(edge_map[n].size()); - unsigned i=0; - for(auto edge : edge_map[n]) - nbrs[i++] = edge.first; - return nbrs; + [[nodiscard]] + std::vector neighbors(const NodeID n) const { + auto iter = neighbors_lazy(n); + return {iter.begin(), iter.end()}; } /// Return the edges - map from NodeID to EdgeID of the current node. - const AdjMap& edges(NodeID n) const { + [[nodiscard]] + std::ranges::input_range auto& edges(const NodeID n) const { + GEL_ASSERT(valid_node_id(n)); return edge_map[n]; } + + [[nodiscard]] std::ranges::input_range + auto shared_neighbors_lazy(const NodeID n0, const NodeID n1) const + { + GEL_ASSERT(valid_node_id(n0) && valid_node_id(n1)); + return edge_map[n0] + | std::views::keys + | std::views::filter([this, n1](const auto& e) { return edge_map[n1].contains(e); }); + } /// Return a vector of shared neighbors. - std::vector shared_neighbors(AMGraph::NodeID n0, AMGraph::NodeID n1) { - auto nbrs0 = neighbors(n0); - auto nbrs1 = neighbors(n1); - std::vector nbr_isect; - set_intersection(begin(nbrs0), end(nbrs0), begin(nbrs1), end(nbrs1), back_inserter(nbr_isect)); - return nbr_isect; - }; + [[nodiscard]] + std::vector shared_neighbors(const NodeID n0, const NodeID n1) const + { + auto iter = shared_neighbors_lazy(n0, n1); + return {iter.begin(), iter.end()}; + } /// Return the number of edges incident on a given node. size_t valence(NodeID n) const { return edge_map[n].size(); } + + void erase_edge(const NodeID n0, const NodeID n1) + { + if (valid_node_id(n0) && valid_node_id(n1)) { + edge_map[n0].erase(n1); + edge_map[n1].erase(n0); + } + } + + void erase_node(const NodeID n0) + { + if (valid_node_id(n0)) { + for (auto neighbor: neighbors_lazy(n0)) { + edge_map[neighbor].erase(n0); + } + edge_map[n0].clear(); + } + } }; @@ -155,7 +188,7 @@ namespace Geometry { /** AMGraph3D extends the AMGraph class by providing attributes for position, edge and node colors. This is mostly for convenience as these attributes could be stored outside the class, but it gives us a concrete 3D graph structure that has many applications. */ - class AMGraph3D: public AMGraph + class AMGraph3D final: public AMGraph { public: /// position attribute for each node diff --git a/src/GEL/Geometry/KDTree.h b/src/GEL/Geometry/KDTree.h index 80fba049..6583968b 100644 --- a/src/GEL/Geometry/KDTree.h +++ b/src/GEL/Geometry/KDTree.h @@ -9,11 +9,10 @@ * @brief KD Tree implementation based on a binary heap. */ -#ifndef __GEOMETRY_KDTREE_H -#define __GEOMETRY_KDTREE_H +#ifndef GEOMETRY_KDTREE_H +#define GEOMETRY_KDTREE_H #include -#include #include #include #include @@ -24,8 +23,10 @@ #pragma warning (disable: 4018) #endif -namespace { - +namespace Geometry +{ +namespace detail +{ /** NQueue is a simple adapter for priority_queue which makes it simple to create an n-element list of items of arbitrary class. There must be a comparison operator for T */ @@ -36,7 +37,10 @@ class NQueue { public: /// Create an NQueue of specified size - NQueue(size_t sz): max_sz(sz) {} + NQueue(size_t sz): max_sz(sz) + { + q.reserve(sz+1); + } /// Push an item onto an NQueue void push(const T& item) { @@ -67,11 +71,8 @@ class NQueue { } }; - -} +} // namespace detail -namespace Geometry -{ template struct KDTreeRecord { using ScalarType = typename KeyT::ScalarType; @@ -155,12 +156,12 @@ namespace Geometry void in_sphere_priv(unsigned n, const KeyType& p, const ScalarType& dist, - std::vector& records) const; + std::vector>& records) const; void m_closest_priv(unsigned n, const KeyType& p, ScalarType& max_dist, - NQueue>& nq) const; + detail::NQueue>& nq) const; /** Finds the optimal discriminator. There are more ways, but this function traverses the vector and finds out what dimension has @@ -240,19 +241,40 @@ namespace Geometry if(nodes.size()>1) { ScalarType max_sq_dist = CGLA::sqr(dist); - std::vector records; + std::vector> records; in_sphere_priv(1,p,max_sq_dist,records); size_t N = records.size(); keys.resize(N); vals.resize(N); for (unsigned i=0;i>& kvs) const + { + assert(is_built); + if(nodes.size()>1) + { + ScalarType max_sq_dist = CGLA::sqr(dist); + in_sphere_priv(1, p, max_sq_dist,kvs); + size_t N = kvs.size(); + return N; + } + return 0; + } /** Find the m elements closest to p and within a distance dist. This function returns a vector of KDTreeRecords in no particular order. This function is likely to be faster than simply @@ -264,7 +286,7 @@ namespace Geometry if(nodes.size()>1) { ScalarType max_sq_dist = CGLA::sqr(dist); - NQueue> nq(m); + detail::NQueue> nq(m); m_closest_priv(1, p, max_sq_dist, nq); nq.to_vector(nv); } @@ -388,12 +410,12 @@ namespace Geometry void KDTree::in_sphere_priv(unsigned n, const KeyType& p, const ScalarType& dist, - std::vector& records) const + std::vector>& records) const { ScalarType this_dist = nodes[n].dist(p); assert(n::m_closest_priv(unsigned n, const KeyType& p, ScalarType& max_dist, - NQueue>& nq) const + detail::NQueue>& nq) const { ScalarType dist = nodes[n].dist(p); assert(n - #include #include using namespace CGLA; -using namespace std; namespace Geometry { - Vec3d QEM::opt_pos(double sv_thresh, const CGLA::Vec3d& p0) const + Vec3d QEM::opt_pos(double QEM_thresh, const CGLA::Vec3d& p0) const { // Compute eigensolution of the symmetric matrix A. This // allows us to factorize it into A = U L U^T and compute @@ -34,14 +31,16 @@ namespace Geometry case 2: U[2] = cross(U[0],U[1]); break; + default: + break; } // For each eigenvalue, we compute the corresponding component // of either the least squares or least norm solution. - double limit = abs(sv_thresh * L[0][0]); + const double limit = std::abs(QEM_thresh * L[0][0]); Vec3d x(0); for(int i=0;i<3;++i) { - if(abs(L[i][i]) -#include -#include +#include - -namespace +namespace Geometry { - inline const CGLA::Mat3x3d direct_product(const CGLA::Vec3d& v0, const CGLA::Vec3d& v1) - { - CGLA::Mat3x3d m; - for(int i=0;i<3;++i) - for(int j=0;j<3;++j) - m[i][j] = v0[i]*v1[j]; - return m; - } +namespace detail +{ + inline CGLA::Mat3x3d direct_product(const CGLA::Vec3d& v0, const CGLA::Vec3d& v1) + { + CGLA::Mat3x3d m; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + m[i][j] = v0[i] * v1[j]; + return m; + } } -namespace Geometry -{ - class QEM - { - CGLA::Mat3x3d A; - CGLA::Vec3d b; - double c; - public: - - QEM(): A(0), b(0), c(0) {} - - QEM(const CGLA::Vec3d& p0, const CGLA::Vec3d& n0, double w=1.0f): - A(direct_product(n0,n0) * w), - b(-2*n0*dot(n0,p0) * w), - c(dot(p0,n0)*dot(p0,n0) * w) {} - - - void operator+=(const QEM& q) - { - A += q.A; - b += q.b; - c += q.c; - } - - float error(const CGLA::Vec3d& p) const - { - return dot(p,A*p) + dot(b,p)+ c; - } - - double determinant() const - { - return CGLA::determinant(A); - } - - const CGLA::Vec3d grad(const CGLA::Vec3d& p) const - { - return CGLA::Vec3d(2*A*p+b); - } - - CGLA::Vec3d opt_pos(double QEM_thresh = 0.5, const CGLA::Vec3d& p0 = CGLA::Vec3d(0.0)) const; - - }; +/// Quadratic Error Metric +class QEM { + CGLA::Mat3x3d A; + CGLA::Vec3d b; + double c; + +public: + QEM() : A(0), b(0), c(0) {} + + /// Construct a QEM from the given position and vector + QEM(const CGLA::Vec3d& p0, const CGLA::Vec3d& n0, double w = 1.0f) : + A(detail::direct_product(n0, n0) * w), + b(-2 * n0 * dot(n0, p0) * w), + c(dot(p0, n0) * dot(p0, n0) * w) {} + + + void operator+=(const QEM& rhs) + { + A += rhs.A; + b += rhs.b; + c += rhs.c; + } + + /// Add the metrics together + QEM operator+(const QEM& rhs) const + { + QEM r = *this; + r += rhs; + return r; + } + + /// Calculate the error for a given position on this QEM + [[nodiscard]] + double error(const CGLA::Vec3d& p) const + { + return CGLA::dot(p, A * p) + CGLA::dot(b, p) + c; + } + + [[nodiscard]] + double determinant() const + { + return CGLA::determinant(A); + } + + [[nodiscard]] + CGLA::Vec3d grad(const CGLA::Vec3d& p) const + { + return CGLA::Vec3d(2 * A * p + b); + } + + /// Calculate the optimal position for a given singularity threshold and vertex coordinate + [[nodiscard]] + CGLA::Vec3d opt_pos(double QEM_thresh = 0.5, const CGLA::Vec3d& p0 = CGLA::Vec3d(0.0)) const; +}; } +// FIXME: remove/move this namespace alias namespace GEO = Geometry; #endif diff --git a/src/GEL/Geometry/etf.h b/src/GEL/Geometry/etf.h index 773e8256..9ca6fa94 100644 --- a/src/GEL/Geometry/etf.h +++ b/src/GEL/Geometry/etf.h @@ -2,110 +2,144 @@ // Created by etoga on 11/6/23. // -#ifndef C_ETF_H -#define C_ETF_H +#ifndef GEOMETRY_ETF_H +#define GEOMETRY_ETF_H #include -#include - -using uint = unsigned int; - -inline -uint pcg_hash(uint input){ - uint state = input * 747796405u + 2891336453u; - uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; - return (word >> 22u) ^ word; +namespace Geometry +{ +using uint = std::uint32_t; + +namespace detail +{ + constexpr + uint pcg_hash(const uint input) + { + const uint state = input * 747796405u + 2891336453u; + const uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + return (word >> 22u) ^ word; + } } -struct Treap{ - uint prio, size = 1u; - uint l=0, r=0, p=0; - Treap(uint x, uint y): prio(pcg_hash(x< arena = {Treap()}; - inline - uint& par(uint i){ - return arena[i].p; + [[nodiscard]] + const uint& parent(const uint i) const + { + return arena[i].parent; + } + + uint& parent(const uint i) + { + return arena[i].parent; + } + + [[nodiscard]] + const uint& left_child(const uint i) const + { + return arena[i].left_child; + } + + uint& left_child(const uint i) + { + return arena[i].left_child; } - inline - uint& lc(uint i){ - return arena[i].l; + [[nodiscard]] + const uint& right_child(const uint i) const + { + return arena[i].right_child; } - inline - uint& rc(uint i){ - return arena[i].r; + uint& right_child(const uint i) + { + return arena[i].right_child; } - inline - bool is_lc(uint i){ - return (par(i) && lc(par(i)) == i); + [[nodiscard]] + bool is_lc(const uint i) const + { + return (parent(i) && left_child(parent(i)) == i); } - inline - uint prio(uint i){ - return arena[i].prio; + [[nodiscard]] + uint priority(const uint i) const + { + return arena[i].priority; } - inline - uint count(uint i){ + [[nodiscard]] + uint count(const uint i) const + { return arena[i].size; } - inline - void update_count(uint i){ - if(i) arena[i].size = 1 + count(lc(i)) + count(rc(i)); + void update_count(uint i) + { + if (i) arena[i].size = 1 + count(left_child(i)) + count(right_child(i)); } - void join(uint& t, uint l, uint r){ - if(!l || !r) t = l ? l:r; - else if (prio(l) > prio(r)){ - join(rc(l), rc(l), r); - par(rc(l)) = l; + void join(uint& t, const uint l, const uint r) + { + if (!l || !r) t = l ? l : r; + else if (priority(l) > priority(r)) { + join(right_child(l), right_child(l), r); + parent(right_child(l)) = l; t = l; } else { - join(lc(r), l, lc(r)); - par(lc(r)) = r; + join(left_child(r), l, left_child(r)); + parent(left_child(r)) = r; t = r; } update_count(t); } // Split s.t. l < t <= r - void split(uint t, uint& l, uint& r){ - l = lc(t); - if(l) par(l) = 0; + void split(uint t, uint& l, uint& r) + { + l = left_child(t); + if (l) parent(l) = 0; r = t; arena[t].size -= count(l); - lc(t) = 0; - while(par(t)){ - if(is_lc(t)){ - if(t == l){ - lc(par(t)) = r; - if(r) par(r) = par(t); - r = par(t); - par(t) = 0; + left_child(t) = 0; + while (parent(t)) { + if (is_lc(t)) { + if (t == l) { + left_child(parent(t)) = r; + if (r) parent(r) = parent(t); + r = parent(t); + parent(t) = 0; } else { - r = par(t); + r = parent(t); } t = r; } else { - if(t == r){ - rc(par(t)) = l; - if(l) par(l) = par(t); - l = par(t); - par(t) = 0; + if (t == r) { + right_child(parent(t)) = l; + if (l) parent(l) = parent(t); + l = parent(t); + parent(t) = 0; } else { - l = par(t); + l = parent(t); } t = l; } @@ -114,24 +148,26 @@ struct ETF{ update_count(t); } - - public: - void reserve(uint size){ + void reserve(const uint size) + { arena.reserve(size); } - uint representative(uint t) { - while(par(t)){ - t = par(t); + [[nodiscard]] + uint representative(uint t) const + { + while (parent(t)) { + t = parent(t); } return t; } - uint accumulate(){ + uint accumulate() + { uint size = arena.size(); - arena.emplace_back(size,size); - if(size>1) { + arena.emplace_back(size, size); + if (size > 1) { uint _; join(_, root, size); root = representative(root); @@ -141,43 +177,49 @@ struct ETF{ return size; } - // Takes indices in the ETF arena - // Returns indices of the new edges in the ETF arena - std::pair insert(const uint u, const uint v){ + /// Takes indices in the ETF arena, returns indices of the new edges in the ETF arena + std::pair insert(const uint u, const uint v) + { uint advance = arena.size(); - arena.emplace_back(u,v); + arena.emplace_back(u, v); uint retreat = arena.size(); - arena.emplace_back(v,u); + arena.emplace_back(v, u); // Do the split - uint A,B,J=0,K=0,L=0; - split(u,A,B); - if(representative(v)==A){ // v left of u - split(v,J,K); + uint A, B, J = 0, K = 0, L = 0; + split(u, A, B); + if (representative(v) == A) { + // v left of u + split(v, J, K); L = B; A = advance; B = retreat; - } else { // u left of v - split(v,K,L); + } else { + // u left of v + split(v, K, L); J = A; A = retreat; B = advance; } - join(K,K,A); - join(J,J,B); - join(J,J,L); - return {advance,retreat}; + join(K, K, A); + join(J, J, B); + join(J, J, L); + return {advance, retreat}; } - bool connected(const uint u, const uint v){ - return representative(u)==representative(v); + [[nodiscard]] + bool connected(const uint u, const uint v) const + { + return representative(u) == representative(v); } - bool connected(const uint u, const uint v, uint& out_tree, uint& out_to_tree) { + bool connected(const uint u, const uint v, uint& out_tree, uint& out_to_tree) const + { out_tree = representative(u); out_to_tree = representative(v); return out_tree == out_to_tree; } }; +} -#endif //C_ETF_H +#endif // GEOMETRY_ETF_H diff --git a/src/GEL/Geometry/graph_skeletonize.cpp b/src/GEL/Geometry/graph_skeletonize.cpp index a93e8d26..3d57c4e5 100644 --- a/src/GEL/Geometry/graph_skeletonize.cpp +++ b/src/GEL/Geometry/graph_skeletonize.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -62,9 +61,7 @@ namespace Geometry { using hrc = chrono::high_resolution_clock; using NodeID = AMGraph::NodeID; - using NodeSetUnordered = unordered_set; - using NodeQueue = queue; - using SepVec = vector; + using SepVec = std::vector; // Convert vector to the simpler output data structure which is simply // a vector of pairs consisting of quality measure and the separator itself. diff --git a/src/GEL/Geometry/graph_skeletonize.h b/src/GEL/Geometry/graph_skeletonize.h index 235e8a84..6d8a2b37 100644 --- a/src/GEL/Geometry/graph_skeletonize.h +++ b/src/GEL/Geometry/graph_skeletonize.h @@ -11,17 +11,10 @@ #include #include -#include "GEL/CGLA/Vec3d.h" +#include // graph related typedefs namespace Geometry { - using NodeID = AMGraph::NodeID; - using NodeSet = AMGraph::NodeSet; - using NodeSetUnordered = std::unordered_set; - using NodeSetVec = std::vector>; - using AttribVecDouble = Util::AttribVec; - using ExpansionMap = std::vector>; - using CapacityVecVec = std::vector>; // A set of graphs of different sizes representing the same original graph. struct MultiScaleGraph { diff --git a/src/GEL/Geometry/graph_util.cpp b/src/GEL/Geometry/graph_util.cpp index e18f3a35..31de64d1 100644 --- a/src/GEL/Geometry/graph_util.cpp +++ b/src/GEL/Geometry/graph_util.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include #include -#include #include using namespace std; @@ -30,30 +30,16 @@ using namespace Util; using namespace HMesh; namespace Geometry { - using NodeID = AMGraph::NodeID; - using NodeSet = AMGraph::NodeSet; - using NodeSetUnordered = unordered_set; - using NodeQueue = queue; - using NodeSetVec = vector>; - using ExpansionMap = std::vector>; - using CapacityVecVec = std::vector>; SkeletonPQElem::SkeletonPQElem(double _pri, AMGraph3D::NodeID _n0, AMGraph3D::NodeID _n1): pri(_pri), n0(_n0), n1(_n1) {} - int test_intersection (const NodeSet& set1, const NodeSet& set2) + int test_intersection(const NodeSet& set1, const NodeSet& set2) { - auto first1 = begin(set1); - auto first2 = begin(set2); - int matches=0; - while (first1!=end(set1) && first2!=end(set2)) - { - if (*first1<*first2) ++first1; - else if (*first2<*first1) ++first2; - else { + int matches = 0; + for (auto& n: set1) { + if (set2.contains(n)) { ++matches; - ++first1; - ++first2; } } return matches; @@ -73,16 +59,10 @@ namespace Geometry { return nbors; } - - NodeSet order(const NodeSetUnordered& s) { - NodeSet _s; - for(const auto n : s) - _s.insert(n); - return _s; + return NodeSet(s.begin(), s.end()); } - Vec3d node_set_barycenter(const AMGraph3D& g, const NodeSet& ns) { Vec3d b(0); for(auto n: ns) diff --git a/src/GEL/Geometry/graph_util.h b/src/GEL/Geometry/graph_util.h index fac6ae91..a44cd66b 100644 --- a/src/GEL/Geometry/graph_util.h +++ b/src/GEL/Geometry/graph_util.h @@ -1,16 +1,15 @@ -// -// graph_abstraction.hpp -// MeshEditE -// -// Created by Jakob Andreas Bærentzen on 30/04/2018. -// Copyright © 2018 J. Andreas Bærentzen. All rights reserved. -// +/// +/// @file graph_util.h +/// MeshEditE +/// +/// Created by Jakob Andreas Bærentzen on 30/04/2018. +/// Copyright © 2018 J. Andreas Bærentzen. All rights reserved. +/// -#ifndef graph_abstraction_hpp -#define graph_abstraction_hpp +#ifndef GEOMETRY_GRAPH_UTIL_H +#define GEOMETRY_GRAPH_UTIL_H #include -#include #include #include #include @@ -18,11 +17,12 @@ namespace Geometry { using AttribVecDouble = Util::AttribVec; - using NodeSetUnordered = std::unordered_set; + using NodeSetUnordered = Util::HashSet; using NodeSet = AMGraph::NodeSet; using NodeSetVec = std::vector>; using ExpansionMap = std::vector>; using CapacityVecVec = std::vector>; + using NodeQueue = std::queue; /// Linear time counting of the number of shared members of set1 and set2. int test_intersection (const AMGraph3D::NodeSet& set1, const AMGraph3D::NodeSet& set2); diff --git a/src/GEL/Geometry/normal.cpp b/src/GEL/Geometry/normal.cpp index a4b2c147..216ffbfb 100644 --- a/src/GEL/Geometry/normal.cpp +++ b/src/GEL/Geometry/normal.cpp @@ -1,39 +1,19 @@ #include +#include -namespace Geometry { +namespace Geometry +{ +using namespace CGLA; - Vec3d smallestEigenVector(const Mat3x3d& matrix) { - Mat3x3d inverseMatrix = invert(matrix); // Invert the matrix - Mat3x3d eigenvectors(0); - Mat3x3d eigenvalues(0); +Vec3d smallestEigenVector(const Mat3x3d& matrix) +{ + Mat3x3d inverseMatrix = invert(matrix); // Invert the matrix + Mat3x3d eigenvectors(0); + Mat3x3d eigenvalues(0); - // Call GEL's power_eigensolution on the inverse matrix - power_eigensolution(inverseMatrix, eigenvectors, eigenvalues, 1); + // Call GEL's power_eigensolution on the inverse matrix + power_eigensolution(inverseMatrix, eigenvectors, eigenvalues, 1); - return eigenvectors[0]; - } - - Vec3d estimateNormal(const std::vector& neighbors) { - Vec3d centroid(0.0f); - for (const auto& point : neighbors) { - centroid += point; - } - centroid /= static_cast(neighbors.size()); - - Mat3x3d covariance(0.0f); - for (const auto& point : neighbors) { - Vec3d diff = point - centroid; - covariance += outer_product(diff, diff); - } - covariance /= static_cast(neighbors.size()); - - Vec3d normal(0.0f); - - normal = smallestEigenVector(covariance); - - if (normal.length() < 1e-8) - std::cout << "Zero normal!" << std::endl; - - return normalize(normal); // Normalize to ensure unit length - } -} \ No newline at end of file + return eigenvectors[0]; +} +} diff --git a/src/GEL/Geometry/normal.h b/src/GEL/Geometry/normal.h index f93bbf12..36a9c5d3 100644 --- a/src/GEL/Geometry/normal.h +++ b/src/GEL/Geometry/normal.h @@ -1,17 +1,36 @@ -#ifndef Normal_h -#define Normal_h +#ifndef GEOMETRY_NORMAL_H +#define GEOMETRY_NORMAL_H #pragma once #include -#include #include -using namespace CGLA; +namespace Geometry +{ -namespace Geometry { - Vec3d estimateNormal(const std::vector& neighbors); +CGLA::Vec3d smallestEigenVector(const CGLA::Mat3x3d& matrix); - Vec3f smallestEigenVector(const Mat3x3f& matrix); +/// Given an input range of coordinates, estimate a normal vector +template +CGLA::Vec3d estimateNormal(Range&& neighbors) +{ + CGLA::Vec3d centroid(0.0f); + for (const auto& point : neighbors) { + centroid += point; + } + centroid /= static_cast(neighbors.size()); + + CGLA::Mat3x3d covariance(0.0f); + for (const auto& point : neighbors) { + CGLA::Vec3d diff = point - centroid; + covariance += outer_product(diff, diff); + } + covariance /= static_cast(neighbors.size()); + + const CGLA::Vec3d normal = smallestEigenVector(covariance); + + return normalize(normal); // Normalize to ensure unit length +} } -#endif \ No newline at end of file +#endif diff --git a/src/GEL/HMesh/Collapse.cpp b/src/GEL/HMesh/Collapse.cpp new file mode 100644 index 00000000..eba709b4 --- /dev/null +++ b/src/GEL/HMesh/Collapse.cpp @@ -0,0 +1,721 @@ +// +// Created by Cem Akarsubasi on 9/10/25. +// + +#include "Collapse.h" + +#include +#include + +namespace HMesh::RSR +{ + +using namespace detail; + +bool vec3_eq(const Vec3& lhs, const Vec3& rhs, double eps = 1e-4) +{ + return lhs.all_le(rhs + Vec3(eps)) && lhs.all_ge(rhs - Vec3(eps)); +} + +bool float_eq(double lhs, double rhs, double eps = 1e-4) +{ + return std::abs(lhs - rhs) < eps; +} + +Vec3 half_edge_direction(const HMesh::Manifold& m, HMesh::HalfEdgeID h) +{ + const auto w = m.walker(h); + const auto current = w.vertex(); + const auto opposing = w.opp().vertex(); + return CGLA::normalize(m.positions[opposing] - m.positions[current]); +} + +Vec3 triangle_normal(const Vec3& p1, const Vec3& p2, const Vec3& p3) +{ + const auto v1 = p2 - p1; + const auto v2 = p3 - p1; + return CGLA::normalize(CGLA::cross(v1, v2)); +} + +// returns 0 at 180 degrees, 1 at 90 (or 270) degrees +double optimize_dihedral(const Vec3& n1, const Vec3& n2) +{ + const auto angle = CGLA::dot(n1, n2) - 1.0; + return std::abs(angle); +} + +// returns 0 for an equilateral triangle +double optimize_angle( + const Vec3& p1, + const Vec3& p2, + const Vec3& p3, + const double& angle_factor, + const double& angle_threshold_penalty, + const double& angle_threshold_cos + ) +{ + const auto e1 = p2 - p1; + const auto e1_len = e1.length(); + const auto e2 = p3 - p1; + const auto e2_len = e2.length(); + + const auto e4 = p3 - p2; + const auto e4_len = e4.length(); + + std::array angles { + dot(e1, e2) / (e1_len * e2_len), + -dot(e2, e4) / (e2_len * e4_len), + dot(-e4, -e1) / (e4_len * e1_len) + }; + + std::array lengths {e4_len, e1_len, e2_len}; + const auto shortest = std::min(e4_len, std::min(e1_len, e2_len)); + double min_angle_ = 0; + double opposing_distance = 0; + for (auto i = 0; i < 3; ++i) { + if (angles[i] < 0) { + continue; + } else if (angles[i] > min_angle_) { + min_angle_ = angles[i]; + opposing_distance = lengths[i]; + } + } + // TODO: this is slow + auto min_angle_acos = std::acos(min_angle_); + //1 - min_angle_; // ol reliable + if (min_angle_ > angle_threshold_cos) { + return angle_threshold_penalty; + } else { + auto score = std::abs( + std::numbers::pi / 3.0 + //0.5 + - min_angle_acos); + return score * shortest * angle_factor; + } +} + + +struct Split { + HMesh::HalfEdgeID h_in = InvalidHalfEdgeID; + HMesh::HalfEdgeID h_out = InvalidHalfEdgeID; + double max_angle = 0; +}; + +struct Triangle { + double shared_length = 0; + Vec3 normal = {0, 0, 0}; + + Triangle() = default; + + Triangle(const Vec3& p1, const Vec3& p2, const Vec3& p3, double shared_length) : + shared_length(shared_length), normal(triangle_normal(p1, p2, p3)) + {} +}; + +auto one_ring_max_angle(const Manifold& manifold, const VertexID vid) -> double +{ + + auto dihedral_from_hedge = [&manifold](HalfEdgeID h) { + Walker w = manifold.walker(h); + auto v1 = w.vertex(); + auto v2 = w.next().vertex(); + auto v3 = w.next().next().vertex(); + auto v4 = w.opp().vertex(); + auto v5 = w.opp().next().vertex(); + auto v6 = w.opp().next().next().vertex(); + if (v1 == InvalidVertexID || + v2 == InvalidVertexID || + v3 == InvalidVertexID || + v4 == InvalidVertexID || + v5 == InvalidVertexID || + v6 == InvalidVertexID) { + GEL_ASSERT(false, "wat"); + return 0.0; + } + auto n0 = triangle_normal( + manifold.positions[v1], + manifold.positions[v2], + manifold.positions[v3]); + auto n1 = triangle_normal( + manifold.positions[v4], + manifold.positions[v5], + manifold.positions[v6]); + return optimize_dihedral(n0, n1); + }; + + double max_angle = 0; + for (const auto h: manifold.incident_halfedges(vid)) { + GEL_ASSERT_NEQ(h, InvalidHalfEdgeID); + max_angle = std::max(max_angle, dihedral_from_hedge(h)); + } + return max_angle; +} + +Split find_edge_pair(const Manifold& m, const VertexID center_idx, const Vec3& v_new_position, + const Vec3& v_old_position, const ReexpandOptions& opts, double angle_threshold_cos) +{ + const auto angle_factor = opts.angle_factor; + const auto angle_threshold_penalty = opts.angle_threshold_penalty; + + // this is getting too complicated + const auto print_hedge = [&m](HalfEdgeID he) { + auto v_from = m.positions[m.walker(he).opp().vertex()]; + auto v_to = m.positions[m.walker(he).vertex()]; + std::cout << v_from << " -> " << v_to << " (" << (v_to - v_from) << ")\n"; + }; + + // Optimize the dihedral value + const auto dihedral_one_ring = [](const Triangle& t1, const Triangle& t2) -> std::pair { + auto d = optimize_dihedral(t1.normal, t2.normal); + auto edge_length = t1.shared_length; + return std::make_pair(d, edge_length); + }; + + struct ExpandResult { + double score = INFINITY; + double max_angle = INFINITY; + }; + const auto expand_score = [&]( + const HalfEdgeID& h_in_opp, + const HalfEdgeID& h_out, + const Point& v_new_position, + const Point& v_old_position) -> ExpandResult { + const auto walker_out = m.walker(h_out); + const auto walker_in_opp = m.walker(h_in_opp); + const auto& v_h_out = m.pos(walker_out.vertex()); + const auto& v_h_in = m.pos(walker_in_opp.vertex()); + + constexpr double EPS = 1e-8; + + const double in_len = std::max((v_old_position - v_h_in).length(), EPS); + const double out_len = std::max((v_new_position - v_h_out).length(), EPS); + const auto tri_center_in = Triangle(v_old_position, v_new_position, v_h_in, in_len); + const auto tri_center_out = Triangle(v_new_position, v_old_position, v_h_out, out_len); + + double angle_cost = 0; + if (angle_factor > 0.0) { + angle_cost += optimize_angle(v_old_position, v_new_position, v_h_in, angle_factor, angle_threshold_penalty, angle_threshold_cos); + angle_cost += optimize_angle(v_new_position, v_old_position, v_h_out,angle_factor, angle_threshold_penalty, angle_threshold_cos); + } + + double dihedral_cost = 0; + double max_angle = 0; + std::array triangles_buffer_; + auto calculate_one_dihedral = [&](const Triangle& tri) { + triangles_buffer_[0] = triangles_buffer_[1]; + triangles_buffer_[1] = tri; + auto [angle, edge_length] = dihedral_one_ring(triangles_buffer_[0], triangles_buffer_[1]); + dihedral_cost += angle * angle * edge_length; + max_angle = std::max(max_angle, angle); + }; + + auto walker_prev = walker_out; + auto walker_next = walker_prev.circulate_vertex_ccw(); + // from out towards in counterclockwise + triangles_buffer_[1] = tri_center_out; + while (walker_prev.halfedge() != walker_in_opp.halfedge()) { + const auto& p2 = m.pos(walker_prev.vertex()); + const auto& p3 = m.pos(walker_next.vertex()); + const auto shared_length = std::max((v_new_position - p3).length(), EPS); + Triangle tri = {v_new_position, p2, p3, shared_length}; + if (angle_factor > 0.0) { + angle_cost += optimize_angle(v_new_position, p2, p3, angle_factor, angle_threshold_penalty, angle_threshold_cos); + } + calculate_one_dihedral(tri); + + walker_prev = walker_prev.circulate_vertex_ccw(); + walker_next = walker_next.circulate_vertex_ccw(); + } + calculate_one_dihedral(tri_center_in); + + // from in towards out counterclockwise + walker_prev = walker_in_opp; + walker_next = walker_in_opp.circulate_vertex_ccw(); + while (walker_prev.halfedge() != walker_out.halfedge()) { + const auto& p2 = m.pos(walker_prev.vertex()); + const auto& p3 = m.pos(walker_next.vertex()); + const auto shared_length = std::max((v_old_position - p3).length(), EPS); + Triangle tri = {v_old_position, p2, p3, shared_length}; + if (angle_factor > 0.0) { + angle_cost += optimize_angle(v_old_position, p2, p3, angle_factor, angle_threshold_penalty, angle_threshold_cos); + } + calculate_one_dihedral(tri); + + walker_prev = walker_prev.circulate_vertex_ccw(); + walker_next = walker_next.circulate_vertex_ccw(); + } + calculate_one_dihedral(tri_center_out); + + const auto [dihedral0_angle, dihedral0_length] = dihedral_one_ring(tri_center_in, tri_center_out); + const auto dihedral0 = dihedral0_angle * dihedral0_angle * dihedral0_length; + max_angle = std::max(max_angle, dihedral0_angle); + + return ExpandResult{ + .score = dihedral_cost + dihedral0 + angle_cost, + .max_angle = max_angle, + }; + }; + + double min_score = INFINITY; + double max_angle = INFINITY; + HalfEdgeID h_in_opp; + HalfEdgeID h_out; + constexpr double dihedral_threshold = 1.75; + + double min_score_alt = INFINITY; + double max_angle_alt = INFINITY; + HalfEdgeID h_in_opp_alt = InvalidHalfEdgeID; + HalfEdgeID h_out_alt = InvalidHalfEdgeID; + + // TODO: ruling out "bad pairs" early can speed this up by a lot + auto v_bar = m.positions[center_idx]; + auto ref_length = (v_old_position - v_new_position).length(); + auto norm = m.normal(center_idx); + auto v3 = v_bar + norm * ref_length; + + for (auto h1 : m.incident_halfedges(center_idx)) { + auto v1 = m.positions[m.walker(h1).vertex()]; + for (auto h2 : m.incident_halfedges(center_idx)) { + // Illegal + if (h1 == h2) { + continue; + } + auto v2 = m.positions[m.walker(h2).vertex()]; + auto plane_norm = CGLA::normalize(CGLA::cross(v3 - v1, v2 - v1)); + //if (CGLA::dot((v_new_position - v_bar), plane_norm) < 0.0 || CGLA::dot((v_old_position - v_bar), plane_norm) > 0.0) + // continue; + + + auto score = expand_score(h1, h2, v_new_position, v_old_position); + if (score.score < min_score) { + min_score = score.score; + max_angle = score.max_angle; + h_in_opp = h1; + h_out = h2; + } + if (score.score < min_score_alt && score.max_angle < dihedral_threshold) { + min_score_alt = score.score; + max_angle_alt = score.max_angle; + h_in_opp_alt = h1; + h_out_alt = h2; + } + if (opts.debug_mask & RE_SPLITS) { + std::cout << "h1: "; + print_hedge(h1); + std::cout << "h2: "; + print_hedge(h2); + std::cout << "score : " << score.score << "\n"; + std::cout << "max angle: " << score.max_angle << "\n"; + } + } + } + if (max_angle > dihedral_threshold && max_angle_alt < dihedral_threshold) { + if (opts.debug_mask & RE_SPLIT_RESULTS) { + std::cout << "using alternative split\n"; + std::cout << "h_in_opp: "; print_hedge(h_in_opp_alt); + std::cout << "h_out: "; print_hedge(h_out_alt); + + } + if (h_out_alt == InvalidHalfEdgeID) { + return {}; + } + return Split{m.walker(h_in_opp_alt).opp().halfedge(), h_out_alt, max_angle_alt}; + } + if (opts.debug_mask & RE_SPLIT_RESULTS) { + std::cout << "using regular split\n"; + std::cout << "h_in_opp: "; print_hedge(h_in_opp); + std::cout << "h_out: "; print_hedge(h_out); + } + if (h_out == InvalidHalfEdgeID) { + return {}; + } + return Split{m.walker(h_in_opp).opp().halfedge(), h_out, max_angle}; +} + +auto angle_flip_check(const Manifold& manifold, const HalfEdgeID he, double angle_threshold) -> bool +{ + if (!manifold.precond_flip_edge(he)) + return false; + const auto angles = [&](HalfEdgeID he) -> std::pair { + auto walker = manifold.walker(he); + auto v1 = manifold.positions[walker.vertex()]; + auto v2 = manifold.positions[walker.next().vertex()]; + auto v3 = manifold.positions[walker.opp().vertex()]; + auto e_shared = CGLA::normalize(v1 - v3); + auto e_next = CGLA::normalize(v2 - v1); + auto e_prev = CGLA::normalize(v2 - v3); + + auto angle1 = CGLA::dot(e_next, -e_shared); + auto angle2 = CGLA::dot(e_prev, e_shared); + + return {angle1, angle2}; + }; + const auto he_opp = manifold.walker(he).opp().halfedge(); + auto [angle1, angle2] = angles(he); + auto [angle3, angle4] = angles(he_opp); + + if (angle1 < 0.0 || angle2 < 0.0 || angle3 < 0.0 || angle4 < 0.0) { + return false; + } + + if (std::acos(angle1) < angle_threshold || + std::acos(angle2) < angle_threshold || + std::acos(angle3) < angle_threshold || + std::acos(angle4) < angle_threshold) { + return true; + } + return false; +} + +auto collapse_points(const std::vector& vertices, const std::vector& normals, + const CollapseOpts& options) -> std::pair +{ + if (options.max_iterations == 0) { + return std::make_pair(Collapse(), PointCloud(vertices, normals)); + } + std::cout << "Collapsing..." << std::endl; + GEL_ASSERT_EQ(vertices.size(), normals.size()); + Util::ImmediatePool pool; + QuadraticCollapseGraph graph; + + // initialize graph + for (auto i = 0UL; i < vertices.size(); ++i) { + graph.add_node(vertices[i], normals[i]); + } + auto indices = [&vertices] { + std::vector temp(vertices.size()); + std::iota(temp.begin(), temp.end(), 0); + return temp; + }(); + + const auto kd_tree = build_kd_tree_of_indices(vertices, indices); + const auto neighbor_map = calculate_neighbors(pool, vertices, kd_tree, options.initial_neighbors); + + // This also initializes distances + for (const auto& neighbors : neighbor_map) { + const NodeID this_id = neighbors[0].id; + for (const auto& neighbor : neighbors | std::views::drop(1)) { + // kNN connection + graph.connect_nodes(this_id, neighbor.id); + } + } + + std::vector> collapses; + + for (size_t iter = 0; iter < options.max_iterations; ++iter) { + // TODO: stricter checking + const size_t max_collapses = + [&]() -> size_t { + if (options.max_collapses != 0) return options.max_collapses; + + return vertices.size() * std::pow(0.5, iter) * options.reduction_per_iteration; + }(); + + std::vector activity; + + size_t count = 0; + while (count < max_collapses) { + count++; + auto [active, latent, active_point_coords, latent_point_coords, v_bar] = graph.collapse_one(); + + activity.emplace_back(active_point_coords, latent_point_coords, v_bar); + } + collapses.emplace_back(std::move(activity)); + std::cout << "Collapsed " << count << " of " << max_collapses << std::endl; + } + return std::make_pair(Collapse {std::move(collapses)}, graph.to_point_cloud()); // TODO +} + +struct PointHash { + size_t operator()(const Point& point) const + { + const auto h1 = std::hash{}(point[0]); + const auto h2 = std::hash{}(point[1]); + const auto h3 = std::hash{}(point[2]); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } +}; + +struct PointEquals { + size_t operator()(const Point& left, const Point& right) const + { + return (left[0] == right[0]) && (left[1] == right[1]) && (left[2] == right[2]); + } +}; + +std::optional find_crossed_edge( + const Manifold& manifold, + const VertexID id, + const Point& active_pos, + const Point& latent_pos, + const ReexpandOptions& opts) +{ + for (auto he: manifold.incident_halfedges(id)) { + auto walker = manifold.walker(he); + auto flip_maybe = walker.next().halfedge(); + auto v1 = manifold.positions[walker.vertex()]; + auto v2 = manifold.positions[walker.next().vertex()]; + auto tangent = normalize(v1 - v2); + if (opts.debug_mask & RE_FIRST_FLIP) { + std::cout << v1 << "\n"; + std::cout << v2 << "\n"; + } + + auto normal = CGLA::normalize(CGLA::cross((active_pos - v1), tangent)); + + // normal of the plane + Vec3 binormal = CGLA::cross(tangent, normal); + // point on the plane + const Point& p0 = (v1 + v2) * 0.5; + const double r = (v1 - v2).length() * 0.5; + + // starting point in our line + const Point& l0 = active_pos; + const Vec3 l = normalize(latent_pos - active_pos); + + auto den = dot(l, binormal); + if (std::abs(den) < 1e-8) { + continue; + } // parallel case + auto len = (latent_pos - active_pos).length(); + auto d = dot((p0 - l0), binormal) / den; + + auto p = l0 + d * l; + auto distance = (p - p0).length(); + if (opts.debug_mask & RE_FIRST_FLIP) { + std::cout << "d : " << d << "\n"; + std::cout << "len: " << len << "\n"; + } + if (distance > r) { + continue; + } + + if (d > 0 && len > (d * 0.90) && manifold.precond_flip_edge(flip_maybe)) { + if (opts.debug_mask & RE_FIRST_FLIP) { + std::cout << "flipped!" << "\n"; + } + return flip_maybe; + } + } + return std::nullopt; +} + +// Fast quick sort implementations from cppreference +namespace +{ + template > + void quick_sort(std::forward_iterator auto first, std::forward_iterator auto last, Comp comp = std::less<>{}) + { + if (first == last) + return; + + auto pivot = *std::next(first, std::distance(first, last) / 2); + auto middle1 = std::partition(first, last, [&](const auto& elem) + { + return comp(elem, pivot); + }); + auto middle2 = std::partition(middle1, last, [&](const auto& elem) + { + return !comp(pivot, elem); + }); + + quick_sort(first, middle1, comp); + quick_sort(middle2, last, comp); + } + + template {})> + void quick_sort(Range&& arr, Comp comp = std::less<>{}) + { + if (std::ranges::distance(arr) == 0) { + return; + } + quick_sort(arr.begin(), arr.end(), comp); + } +} + +auto reexpand_points(Manifold& manifold, Collapse&& collapse, const ReexpandOptions& opts) -> void +{ + std::cout << "reexpanding" << std::endl; + const auto& manifold_positions = manifold.positions; + + // TODO: Replace this with the collection in Util + // TODO: factor this out + std::unordered_multimap point_to_manifold_ids; + for (auto manifold_vid : manifold.vertices()) { + auto pos = manifold_positions[manifold_vid]; + point_to_manifold_ids.emplace(pos, manifold_vid); + } + auto position_to_manifold_iter = [&](const Point& point) { + auto [fst, snd] = point_to_manifold_ids.equal_range(point); + return std::ranges::subrange(fst, snd) | std::views::values; + }; + using IndexIter = decltype(position_to_manifold_iter(std::declval())); + + // insert latent point to stored latent position + // update active point position to the stored coordinate + + double angle_threshold_cos = std::cos(opts.angle_threshold); + // Now we need to consider two position candidates + + size_t expansion_failures = 0; + size_t bad_expansions = 0; + size_t flips = 0; + int iteration = 0; + std::vector one_ring; + std::vector circle; + std::vector two_ring; + for (const auto& collapse_iter : collapse.collapses | std::views::reverse) { + for (auto single_collapse : collapse_iter | std::views::reverse) { + iteration++; + // find the manifold_ids for the active vertex + + const auto active_pos = single_collapse.active_point_coords; + const auto latent_pos = single_collapse.latent_point_coords; + const auto v_bar = single_collapse.v_bar; + // FIXME: Debug info + if (opts.debug_mask & RE_ITERATION) { + std::cout << "--------------------------------\n"; + std::cout << "@iteration: " << iteration << "\n"; + std::cout << "active pos: " << active_pos << "\n"; + std::cout << "latent pos: " << latent_pos << "\n"; + std::cout << "combin pos: " << v_bar << "\n"; + } + if (opts.debug_mask & RE_MARK_SPLITS) { + manifold.add_face({active_pos, latent_pos, v_bar}); + } + if (opts.debug_mask & RE_FIRST_FLIP) { + std::cout << "First flip:\n"; + } + // repair local geometry maybe + const auto manifold_ids = position_to_manifold_iter(v_bar); + for (const auto id : manifold_ids) { + auto maybe1 = find_crossed_edge(manifold, id, latent_pos, active_pos, opts); + if (maybe1) { + manifold.flip_edge(*maybe1); + } + auto maybe2 = find_crossed_edge(manifold, id, active_pos, latent_pos, opts); + if (maybe2) { + manifold.flip_edge(*maybe2); + } + + manifold.positions[id] = active_pos; + } + + // actually do the expansion + const auto new_vid = [&]() -> VertexID { + // we want to get as close to 90 degrees as possible here + for (const auto this_vert : manifold_ids) { + const auto candidate = find_edge_pair(manifold, this_vert, latent_pos, active_pos, opts, angle_threshold_cos); + if (candidate.h_in != InvalidHalfEdgeID) { + const auto vnew = manifold.split_vertex(candidate.h_in, candidate.h_out); + GEL_ASSERT_NEQ(vnew, InvalidVertexID); + return vnew; + } + } + return InvalidVertexID; + }(); + + if (new_vid == InvalidVertexID) { + expansion_failures++; + continue; + } + + manifold.positions[new_vid] = latent_pos; + for (const auto id : manifold_ids) { + point_to_manifold_ids.emplace(active_pos, id); + } + point_to_manifold_ids.emplace(latent_pos, new_vid); + point_to_manifold_ids.erase(v_bar); + + if (opts.debug_mask & RE_SECOND_FLIP) { + std::cout << "Second flip:\n"; + } + + + one_ring.clear(); + circle.clear(); + two_ring.clear(); + circulate_vertex_ccw(manifold, new_vid, [&](Walker& w) { + const HalfEdgeID one_ring_he = w.halfedge(); + const HalfEdgeID circle_he = w.next().halfedge(); + const HalfEdgeID two_ring_he = w.next().opp().next().halfedge(); + const HalfEdgeID two_ring_he2 = w.next().opp().prev().halfedge(); + + one_ring.push_back(one_ring_he); + circle.push_back(circle_he); + two_ring.push_back(two_ring_he); + two_ring.push_back(two_ring_he2); + }); + + // sort from longest to shortest edge + const auto& manifold_cref = manifold; + auto cmp = [&manifold_cref](const HalfEdgeID& e1, const HalfEdgeID& e2) -> bool { + // TODO: There is a serious soundness issue here + GEL_ASSERT(manifold_cref.in_use(e1), "%ld", e1.get_index()); + GEL_ASSERT(manifold_cref.in_use(e2), "%ld", e2.get_index()); + auto len1 = manifold_cref.length(e1); + auto len2 = manifold_cref.length(e2); + return len1 < len2; + }; + + for (int i = 0; i < opts.refinement_iterations; ++i) { + auto threshold = opts.refinement_angle_threshold; + quick_sort(one_ring, cmp); + for (HalfEdgeID h : one_ring | std::views::reverse) { + bool flipped = angle_flip_check(manifold, h, M_PI / 180.0 * threshold); + if (flipped) { + manifold.flip_edge(h); + } + flips += flipped; + } + quick_sort(circle, cmp); + for (HalfEdgeID h : circle | std::views::reverse) { + bool flipped = angle_flip_check(manifold, h, M_PI / 180.0 * threshold); + if (flipped) { + manifold.flip_edge(h); + } + flips += flipped; + } + quick_sort(two_ring, cmp); + for (HalfEdgeID h : two_ring | std::views::reverse) { + bool flipped = angle_flip_check(manifold, h, M_PI / 180.0 * threshold); + if (flipped) { + manifold.flip_edge(h); + } + flips += flipped; + } + } + + if (opts.debug_mask & RE_ERRORS) { + const auto v_new_max_angle = one_ring_max_angle(manifold, new_vid); + const auto v_old_max_angle = one_ring_max_angle(manifold, manifold_ids.front()); + if (v_new_max_angle > opts.angle_stop_threshold || v_old_max_angle > opts.angle_stop_threshold) { + bad_expansions++; + if (opts.debug_mask & RE_ERRORS) { + auto normal = manifold.normal(new_vid); + manifold.add_face({active_pos, latent_pos, v_bar + (active_pos-latent_pos).length() * normal}); + std::cout << "v_new max angle: " << v_new_max_angle << "\n"; + std::cout << "v_old max angle: " << v_old_max_angle << "\n"; + std::cout << "failed at iteration " << iteration << "\n"; + } + if (opts.early_stop_at_error && opts.stop_at_error == 0) { + goto EXIT; + } + if (opts.stop_at_error > 0 && opts.stop_at_error == bad_expansions) { + goto EXIT; + } + } + } + + if (opts.stop_at_iteration > 0 && iteration == opts.stop_at_iteration) { + std::cout << "stopped early at " << iteration << "\n"; + goto EXIT; + } + } + } + EXIT: + std::cout << "flips: " << flips << "\n"; + std::cerr << "failures: " << expansion_failures << "\n"; +} + +} diff --git a/src/GEL/HMesh/Collapse.h b/src/GEL/HMesh/Collapse.h new file mode 100644 index 00000000..285cc51b --- /dev/null +++ b/src/GEL/HMesh/Collapse.h @@ -0,0 +1,414 @@ +// +// Created by Cem Akarsubasi on 7/11/25. +// Some Collapse data structures and functions that can be separated from RsR + +#ifndef GEL_HMESH_COLLAPSE_H +#define GEL_HMESH_COLLAPSE_H + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include + +#include + + +namespace HMesh::RSR +{ +using Vec3 = CGLA::Vec3d; +using Point = Vec3; +using NodeID = size_t; +using Geometry::AMGraph; +using Geometry::AMGraph3D; + +enum class Distance { + Euclidean, + Tangent, +}; + +struct PointCloud { + std::vector points; + std::vector normals; + std::vector indices; +}; + +struct CollapseOpts { + size_t max_iterations = 4; + double reduction_per_iteration = 0.5; + size_t initial_neighbors = 5; + size_t max_collapses = 0; + Distance distance = Distance::Euclidean; +}; + +/// @brief Calculate projection distance +/// @param edge: the edge to be considered +/// @param this_normal: normal of one vertex +/// @param neighbor_normal: normal of another vertex +/// @return projection distance +inline double tangent_space_distance(const Vec3& edge, const Vec3& this_normal, const Vec3& neighbor_normal) +{ + const double euclidean_distance = edge.length(); + if (std::abs(dot(this_normal, neighbor_normal)) < std::cos(15. / 180. * M_PI)) + return euclidean_distance; + const double neighbor_normal_length = dot(edge, neighbor_normal); + const double normal_length = dot(edge, this_normal); + const double projection_dist = (std::sqrt((euclidean_distance * euclidean_distance) - (normal_length * normal_length)) + + std::sqrt((euclidean_distance * euclidean_distance) - (neighbor_normal_length * neighbor_normal_length))) * 0.5; + return projection_dist; +} + +enum DebugMask { + RE_NONE = 0, + RE_ITERATION = 1 << 0, + RE_SPLITS = 1 << 1, + RE_SPLIT_RESULTS = 1 << 2, + RE_ERRORS = 1 << 3, + RE_FIRST_FLIP = 1 << 4, + RE_SECOND_FLIP = 1 << 5, + RE_MARK_SPLITS = 1 << 6, + RE_MARK_BAD_SPLITS = 1 << 7, +}; + +struct ReexpandOptions { + bool enabled = true; + // DEBUG OPTIONS + + bool debug_print = false; + bool early_stop_at_error = false; + int debug_mask = RE_NONE; + int stop_at_error = 0; + size_t stop_at_iteration = 0; + double angle_stop_threshold = 1.25; + unsigned refinement_iterations = 1; + + double angle_factor = 1; + /// minimum angle to allow a triangle + double angle_threshold = std::numbers::pi / 180.0 * 3.0; + + double angle_threshold_penalty = 0.1; + double refinement_angle_threshold = 22.5; +}; + +inline auto lerp(const Vec3& v1, const Vec3& v2, double t) -> Vec3 +{ + return v1 * (1.0 - t) + v2 * t; +} + +inline double triangle_area(const Point& p1, const Point& p2, const Point& p3) +{ + const auto e1 = p2 - p1; + const auto e2 = p3 - p1; + return CGLA::length(CGLA::cross(e1, e2)) / 2.0; +} + +struct RawCollapse { + NodeID active; + NodeID latent; + Point active_point_coords; + Point latent_point_coords; + Point v_bar; +}; + +// Fat 72 bytes +struct SingleCollapse { + //NodeID active = InvalidNodeID; + //NodeID latent = InvalidNodeID; + /// Old coordinates of the active point + Point active_point_coords; + /// Old coordinates of the latent point + Point latent_point_coords; + /// Current coordinates of the active point + Point v_bar; +}; +struct QuadraticCollapseGraph : AMGraph { +private: + struct Vertex { + Point position; + Vec3 normal; + GEO::QEM qem; + double weight = 1; + }; + + struct Edge { + NodeID from; + NodeID to; + double dist; + + bool operator==(const Edge& rhs) const + { + return (from == rhs.from && to == rhs.to) || (from == rhs.to && to == rhs.from); + } + + bool operator<(const Edge& other) const + { + return dist < other.dist; + } + }; + + struct edge_t { + NodeID from; + NodeID to; + + bool operator==(const edge_t& rhs) const = default; + }; + + struct edge_t_hash { + auto operator()(const edge_t& edge) const -> std::size_t + { + std::hash h; + return h(edge.from) ^ (h(edge.to) << 1); + } + }; + +public: + Util::AttribVec m_vertices; +private: + Util::BTreeSet m_collapse_queue; + Util::AttribVec m_edges; + +public: + auto add_node(const Point& position, const Vec3& normal) -> NodeID + { + const NodeID n = AMGraph::add_node(); + const auto qem = Geometry::QEM(position, normal); + m_vertices[n] = Vertex{.position = position, .normal = normal, .qem = qem}; + return n; + } + + auto connect_nodes(const NodeID n0, const NodeID n1) -> EdgeID + { + if (n1 > n0) { + const EdgeID e = AMGraph::connect_nodes(n0, n1); + auto [_, dist] = distance_function(n0, n1); + m_collapse_queue.emplace(n0, n1, dist); + m_edges[e] = dist; + return e; + } + return InvalidEdgeID; + } + + auto collapse_one() -> RawCollapse + { + while (!m_collapse_queue.empty()) { + auto edge_ = m_collapse_queue.begin(); + auto edge = *edge_; + m_collapse_queue.erase(edge_); + //collapses++; + const auto active = edge.from; + const auto latent = edge.to; + + if (AMGraph::find_edge(active, latent) != AMGraph::InvalidEdgeID) { + + const auto active_coords = m_vertices[active].position; + const auto latent_coords = m_vertices[latent].position; + + GEL_ASSERT_FALSE(CGLA::any(active_coords, [](auto d){ return std::isnan(d); })); + GEL_ASSERT_FALSE(CGLA::any(latent_coords, [](auto d){ return std::isnan(d); })); + + // recalculate current edges + for (auto v : AMGraph::neighbors_lazy(active)) { + auto v0 = std::min(v, active); + auto v1 = std::max(v, active); + auto edge_id = find_edge(v0, v1); + auto dist = m_edges[edge_id]; + m_collapse_queue.extract(Edge{v0, v1, dist}); + } + + for (auto v : AMGraph::neighbors_lazy(latent)) { + auto v0 = std::min(v, latent); + auto v1 = std::max(v, latent); + auto edge_id = find_edge(v0, v1); + auto dist = m_edges[edge_id]; + m_collapse_queue.extract(Edge{v0, v1, dist}); + } + + double total_weight = m_vertices[latent].weight + m_vertices[active].weight; + const auto new_normal = + lerp(m_vertices[latent].normal, m_vertices[active].normal, m_vertices[active].weight / total_weight); + const auto v_bar = + lerp(m_vertices[latent].position, m_vertices[active].position, m_vertices[active].weight / total_weight); + m_vertices[active].position = v_bar; + m_vertices[active].normal = new_normal; + m_vertices[active].weight += m_vertices[latent].weight; + //m_vertices[active].normal = m_vertices[latent].normal; + //m_vertices[active].qem += m_vertices[latent].qem; + m_vertices[latent].position = Point(std::numeric_limits::signaling_NaN()); + m_vertices[latent].normal = Vec3(std::numeric_limits::signaling_NaN()); + + // recalculate current edges + for (auto v : AMGraph::neighbors_lazy(active)) { + auto [v0, v1] = std::minmax(v, active); + connect_nodes(v0, v1); + } + + for (auto v : AMGraph::neighbors_lazy(latent)) { + auto [v0, v1] = std::minmax(v, active); + connect_nodes(v0, v1); + } + AMGraph::erase_node(latent); + + return RawCollapse{ + .active = active, + .latent = latent, + .active_point_coords = active_coords, + .latent_point_coords = latent_coords, + .v_bar = v_bar + }; + } + } + GEL_ASSERT(false); + return RawCollapse{}; + } + + /// Return the remaining points + auto to_point_cloud() -> PointCloud + { + std::vector points; + std::vector normals; + std::vector indices; + for (auto i = 0UL; i < m_vertices.size(); ++i) { + if (!m_vertices[i].position.any([](const double e) { return std::isnan(e); })) { + points.emplace_back(m_vertices[i].position); + normals.emplace_back(m_vertices[i].normal); + indices.emplace_back(i); + } + } + return PointCloud{std::move(points), std::move(normals), std::move(indices)}; + } + +private: + + [[nodiscard]] + std::pair distance_function(const NodeID n0, const NodeID n1) const + { + GEL_ASSERT(valid_node_id(n0) && valid_node_id(n1)); + if (valid_node_id(n0) && valid_node_id(n1)) { + const auto& v0 = m_vertices[n0]; + const auto& v1 = m_vertices[n1]; + + const auto total_weight = v0.weight + v1.weight; + const auto center = lerp(v0.position, v1.position, v0.weight / total_weight); + const auto tangent_distance = + tangent_space_distance(v0.position - v1.position, v0.normal, v1.normal); + + //const auto error = qem.error(clamped); + return std::make_pair(center, tangent_distance * total_weight); + } else { + return std::make_pair(CGLA::Vec3d(), CGLA::CGLA_NAN); + } + } +}; + +/// Contains data needed for a reexpansion +struct Collapse { + std::vector> collapses; +}; + +inline auto create_collapse(std::vector&& collapses) -> Collapse +{ + return Collapse{.collapses = {std::move(collapses)}}; +} + +using Tree = Geometry::KDTree; +using Record = Geometry::KDTreeRecord; + +struct NeighborInfo { + NodeID id; + double distance; + + explicit NeighborInfo(const Record& record) noexcept : id(record.v), distance(std::sqrt(record.d)) + {} +}; + +//using NeighborArray = Util::InplaceVector; +using NeighborArray = std::vector; + +using NeighborMap = std::vector; + +template +Tree build_kd_tree_of_indices(const std::vector& vertices, const Indices& indices) +{ + Tree kd_tree; + for (const auto idx : indices) { + kd_tree.insert(vertices.at(idx), idx); + } + kd_tree.build(); + return kd_tree; +} + +/// @brief k nearest neighbor search +/// @param query: the coordinate of the point to be queried +/// @param kdTree: kd-tree for knn query +/// @param num: number of nearest neighbors to be queried +/// @param neighbors: [OUT] indices of k nearest neighbors +inline void knn_search(const Point& query, const Tree& kdTree, const int num, NeighborArray& neighbors) +{ + // It might be a better idea for the caller to handle this to reduce some clutter + std::vector records = kdTree.m_closest(num, query, INFINITY); + std::sort_heap(records.begin(), records.end()); + + for (auto record : records) { + neighbors.emplace_back(record); + } +} + +template +auto calculate_neighbors( + Util::IExecutor& pool, + const std::vector& vertices, + const Indices& indices, + const Tree& kdTree, + const int k, + NeighborMap&& neighbors_memoized = NeighborMap()) + -> NeighborMap +{ + if (neighbors_memoized.empty()) { + neighbors_memoized = NeighborMap(indices.size()); + for (auto& neighbors : neighbors_memoized) { + neighbors.reserve(k + 2); + } + } else if (neighbors_memoized.at(0).capacity() < k) { + for (auto &neighbors : neighbors_memoized) { + neighbors.reserve(k + 2); + } + } + + auto cache_kNN_search = [&kdTree, k, &vertices](auto index, auto& neighbor) { + auto vertex = vertices.at(index); + knn_search(vertex, kdTree, k, neighbor); + }; + Util::Parallel::foreach2(pool, indices, neighbors_memoized, cache_kNN_search); + return neighbors_memoized; +} + +inline auto calculate_neighbors( + Util::IExecutor& pool, + const std::vector& vertices, + const Tree& kdTree, + const int k, + NeighborMap&& neighbors_memoized = NeighborMap()) + -> NeighborMap +{ + const auto indices = std::ranges::iota_view(0UL, vertices.size()); + return calculate_neighbors(pool, vertices, indices, kdTree, k, std::move(neighbors_memoized)); +} + +auto collapse_points( + const std::vector& vertices, + const std::vector& normals, + const CollapseOpts& options +) -> std::pair; + +auto reexpand_points(Manifold& manifold, Collapse&& collapse, const ReexpandOptions& opts = ReexpandOptions()) -> void; + +} // namespace HMesh + +#endif // GEL_HMESH_COLLAPSE_H diff --git a/src/GEL/HMesh/Manifold.cpp b/src/GEL/HMesh/Manifold.cpp index c45761fc..922285c4 100644 --- a/src/GEL/HMesh/Manifold.cpp +++ b/src/GEL/HMesh/Manifold.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include #include #include @@ -129,7 +131,7 @@ namespace HMesh HalfEdgeID he = kernel.out(hov); HalfEdgeID last = he; do { - assert(kernel.vert(kernel.opp(he)) == hov); + GEL_ASSERT_EQ(kernel.vert(kernel.opp(he)), hov); kernel.set_vert(kernel.opp(he), hv); he = kernel.next(kernel.opp(he)); } @@ -204,7 +206,7 @@ namespace HMesh h = kernel.next(h); } kernel.set_face(h, f2); - assert(h != h1); + GEL_ASSERT_NEQ(h, h1); // create a new halfedge hb to connect v0 and v1. HalfEdgeID hb = kernel.add_halfedge(); @@ -218,11 +220,11 @@ namespace HMesh glue(ha, hb); // assert sanity of operation - assert(kernel.next(kernel.opp(kernel.prev(h1))) == h0); - assert(kernel.next(kernel.opp(kernel.prev(h0))) == h1); - assert(kernel.face(kernel.next(hb)) == f2); - assert(kernel.face(kernel.next(kernel.next(hb))) == f2); - assert(kernel.face(hb) == f2); + GEL_ASSERT_EQ(kernel.next(kernel.opp(kernel.prev(h1))), h0); + GEL_ASSERT_EQ(kernel.next(kernel.opp(kernel.prev(h0))), h1); + GEL_ASSERT_EQ(kernel.face(kernel.next(hb)), f2); + GEL_ASSERT_EQ(kernel.face(kernel.next(kernel.next(hb))), f2); + GEL_ASSERT_EQ(kernel.face(hb), f2); // return handle to newly created face return f2; @@ -384,7 +386,40 @@ namespace HMesh add_boundary_face(hn, h_out); return vn; - // xor + } else if (kernel.face(h_out) == InvalidFaceID) { + auto h_out_opp = kernel.opp(h_out); + auto f_dummy = add_boundary_face(kernel.prev(h_out), h_out); + + GEL_ASSERT_NEQ(kernel.face(h_in), InvalidFaceID); + GEL_ASSERT_NEQ(kernel.face(h_out_opp), InvalidFaceID); + + const auto vn = slit_vertex_impl(h_in, kernel.opp(h_out_opp)); + GEL_ASSERT_NEQ(vn, InvalidVertexID); + + const auto f = close_hole(kernel.out(vn)); + + split_face_by_edge(f, v, vn); + + GEL_ASSERT(remove_face(f_dummy)); + + return vn; + } else if (kernel.face(h_in) == InvalidFaceID) { + auto h_in_opp = kernel.opp(h_in); + auto f_dummy = add_boundary_face(h_in, kernel.next(h_in)); + + GEL_ASSERT_NEQ(kernel.face(h_in_opp), InvalidFaceID); + GEL_ASSERT_NEQ(kernel.face(h_out), InvalidFaceID); + + const auto vn = slit_vertex_impl(kernel.opp(h_in_opp), h_out); + GEL_ASSERT_NEQ(vn, InvalidVertexID); + + const auto f = close_hole(kernel.out(vn)); + + split_face_by_edge(f, v, vn); + + GEL_ASSERT(remove_face(f_dummy)); + + return vn; } else if ((kernel.face(kernel.opp(h_in)) == InvalidFaceID) != (kernel.face(kernel.opp(h_out)) == InvalidFaceID)) { const auto v_new = slit_vertex_impl(h_in, h_out); GEL_ASSERT_NEQ(v_new, InvalidVertexID); @@ -409,6 +444,9 @@ namespace HMesh const auto vn = slit_vertex_impl(h_in, h_out); GEL_ASSERT_NEQ(vn, InvalidVertexID); + GEL_ASSERT_NEQ(kernel.face(h_in), InvalidFaceID); + GEL_ASSERT_NEQ(kernel.face(h_out), InvalidFaceID); + const auto f = close_hole(kernel.out(vn)); split_face_by_edge(f, v, vn); @@ -720,7 +758,7 @@ namespace HMesh bool Manifold::merge_faces(FaceID f, HalfEdgeID h) { //assert that we're merging a valid face with the corresponding halfedge - assert(kernel.face(h) == f); + GEL_ASSERT_EQ(kernel.face(h), f); HalfEdgeID ho = kernel.opp(h); FaceID fo = kernel.face(ho); @@ -750,7 +788,7 @@ namespace HMesh HalfEdgeID hx = hon; - assert(kernel.face(hx) == fo); + GEL_ASSERT_EQ(kernel.face(hx), fo); while(kernel.face(hx) != f){ kernel.set_face(hx, f); hx = kernel.next(hx); @@ -988,9 +1026,13 @@ namespace HMesh } void Manifold::merge(const Manifold& mergee) { - map fmap; - map hmap; - map vmap; + Util::HashMap fmap; + Util::HashMap hmap; + Util::HashMap vmap; + + fmap.reserve(kernel.allocated_faces()); + hmap.reserve(kernel.allocated_halfedges()); + vmap.reserve(kernel.allocated_vertices()); for(auto v: mergee.vertices()) vmap[v] = kernel.add_vertex(); @@ -1036,9 +1078,9 @@ namespace HMesh VertexID hnv = kernel.vert(hn); FaceID f = kernel.face(h); - assert(ho != hn); - assert(h != hno); - assert(hv != hnv); + GEL_ASSERT_NEQ(ho, hn); + GEL_ASSERT_NEQ(h , hno); + GEL_ASSERT_NEQ(hv, hnv); // glue opposite halfedge to halfedge opposite next halfedge, obsoleting h and hn from loop glue(ho, hno); @@ -1619,6 +1661,7 @@ namespace HMesh Manifold::Vec Manifold::area_normal(FaceID f) const { + GEL_ASSERT_NEQ(f, InvalidFaceID); std::vector vertices; Vec c(0.0); int k = circulate_face_ccw(*this, f, [&](VertexID vid) { diff --git a/src/GEL/HMesh/Manifold.h b/src/GEL/HMesh/Manifold.h index f6f15cfe..d50aef46 100644 --- a/src/GEL/HMesh/Manifold.h +++ b/src/GEL/HMesh/Manifold.h @@ -105,7 +105,7 @@ namespace HMesh /// @brief initializer list specialization for add_face // FIXME: initializer lists do not implement the range concept directly in C++20 // FIXME: this can be removed in C++26 - FaceID add_face(std::initializer_list&& points) + FaceID add_face(std::initializer_list points) { const std::span s = points; return add_face(s); diff --git a/src/GEL/HMesh/RsR.cpp b/src/GEL/HMesh/RsR.cpp index 28b0eae5..13957011 100644 --- a/src/GEL/HMesh/RsR.cpp +++ b/src/GEL/HMesh/RsR.cpp @@ -1,5 +1,8 @@ #include +#include +#include #include "RsR.h" +#include static bool isGTNormal = true; static bool isEuclidean = true; static bool isFaceLoop = true; @@ -14,7 +17,7 @@ static std::string model_path; static std::string root_path; static std::string model_name; static std::string mode; -static RsR_Timer recon_timer; +static Util::RSRTimer recon_timer; static int bettiNum_1 = 0; @@ -381,7 +384,7 @@ float find_components(std::vector& vertices, // Remove Edge Longer than threshold std::vector edge_rm_v_id1, edge_rm_v_id2; for (NodeID i = 0; i < components.no_nodes(); i++) { - auto& edges = components.edges(i); + auto edges = components.edges(i); for (const auto& pair : edges) { NodeID vertex1 = i; NodeID vertex2 = pair.first; @@ -2224,7 +2227,7 @@ void reset_static() { root_path = ""; model_name = ""; mode = ""; - recon_timer = RsR_Timer(); + recon_timer = Util::RSRTimer(); bettiNum_1 = 0; } @@ -2494,6 +2497,7 @@ void reconstruct_single(HMesh::Manifold& output, std::vector& org_vertice } recon_timer.end("algorithm"); + recon_timer.end("Whole process"); std::string line(40, '='); std::cout << line << std::endl << std::endl; recon_timer.show(); diff --git a/src/GEL/HMesh/RsR2.cpp b/src/GEL/HMesh/RsR2.cpp new file mode 100644 index 00000000..8018214d --- /dev/null +++ b/src/GEL/HMesh/RsR2.cpp @@ -0,0 +1,1882 @@ +#include +#include +#include +#include +#include // std::views +#include +#include + +#include "GEL/Util/InplaceVector.h" + +namespace HMesh::RSR +{ +using namespace detail; + +using ThreadPool = Util::ImmediatePool; +using Geometry::estimateNormal; +using namespace ::HMesh; +using namespace Util::Ranges; +using Util::AttribVec; +using HMesh::Manifold; + +using FaceType = std::array; + +struct FaceComparator { + constexpr bool operator()(const std::pair& left, + const std::pair& right) const + { + return (left.second) > (right.second); + } +}; + +using FacePriorityQueue = std::priority_queue< + std::pair, + std::vector>, + FaceComparator>; + +struct TEdge { + NodeID from; + NodeID to; +}; +struct PEdgeLength { + TEdge edge; + float length; +}; + +struct FaceConnectionKey { + uint tree_id; + uint to_tree_id; + + constexpr bool operator==(const FaceConnectionKey& other) const noexcept + { + return tree_id == other.tree_id && to_tree_id == other.to_tree_id; + } +}; + +struct FaceConnectionKeyHasher { + std::size_t operator()(const FaceConnectionKey& self) const noexcept + { + constexpr std::hash hasher; + return (hasher(self.tree_id) << 1) ^ hasher(self.to_tree_id); + } +}; + +using FacePair = std::pair; +using NeighborPair = std::pair; + +constexpr bool edge_comparator(const PEdgeLength& l, const PEdgeLength& r) +{ + return l.length < r.length; +} + +constexpr bool face_comparator(const FacePair& l, const FacePair& r) +{ + return l.first > r.first; +} + +constexpr bool neighbor_comparator(const NeighborPair& l, const NeighborPair& r) +{ + return l.first > r.first; +} + +/// @brief Calculate the reference vector for the rotation system +/// @param normal: normal direction for the target vertex +/// @return the reference vector +constexpr Vec3 calculate_ref_vec(const Vec3& normal) +{ + constexpr double eps = 1e-6; + if (normal[2] == 1.0) + return Vec3(0.0, 1.0, 0.0);; + const double second = (normal[1] == 0.0) ? eps : normal[1]; + const auto ref_vec = Vec3(0.0, -normal[2] / second, 1.0); + return CGLA::normalize(ref_vec); +} + +/// @brief Calculate the radian given the reference vector +/// @param branch: vector of the outgoing edge +/// @param normal: normal of the root vertex +/// @param ref_vec: the reference vector +/// @return radian +double cal_radians_3d(const Vec3& branch, const Vec3& normal, const Vec3& ref_vec) +{ + const Vec3 proj_vec = branch - dot(normal, branch) * normal; + const double proj_vec_len = proj_vec.length(); + if (proj_vec_len < 1e-8) + return 0.; + + const Vec3 proj_ref = ref_vec - dot(normal, ref_vec) * normal; + const auto value = std::clamp( + dot(proj_vec, proj_ref) / proj_vec_len / + proj_ref.length(), -1.0, 1.0); + // We clamped value to [-1,1], so radian cannot be NaN unless normal is 0 + double radian = std::acos(value); + if (dot(cross(proj_vec, proj_ref), normal) > 0.0) + radian = 2.0 * M_PI - radian; + return radian; +} + +/// @brief Calculate the radian in the rotation system +/// @param branch: vector of the outgoing edge +/// @param normal: normal of the root vertex +/// @return radian +double cal_radians_3d(const Vec3& branch, const Vec3& normal) +{ + const auto ref_vec = calculate_ref_vec(normal); + return cal_radians_3d(branch, normal, ref_vec); +} + +/// @brief Calculate projection distance +/// @param edge: the edge to be considered +/// @param this_normal: normal of one vertex +/// @param neighbor_normal: normal of another vertex +/// @return projection distance +double cal_proj_dist(const Vec3& edge, const Vec3& this_normal, const Vec3& neighbor_normal) +{ + const double euclidean_distance = edge.length(); + if (std::abs(dot(this_normal, neighbor_normal)) < std::cos(15. / 180. * M_PI)) + return euclidean_distance; + const double neighbor_normal_length = dot(edge, neighbor_normal); + const double normal_length = dot(edge, this_normal); + const double projection_dist = (std::sqrt((euclidean_distance * euclidean_distance) - (normal_length * normal_length)) + + std::sqrt((euclidean_distance * euclidean_distance) - (neighbor_normal_length * neighbor_normal_length))) * 0.5; + return projection_dist; +} + +void filter_out_cross_connection( + NeighborArray& neighbors, + const std::vector& normals, + const NodeID this_idx, + const double cross_conn_thresh, + const bool is_euclidean) +{ + const auto& this_normal = normals[this_idx]; + std::erase_if(neighbors, [&](const auto& neighbor_info) { + const auto& neighbor_normal = normals[neighbor_info.id]; + const double cos_theta = dot(this_normal, neighbor_normal); + const double cos_thresh = + (is_euclidean) ? 0.0 : std::cos(cross_conn_thresh / 180. * M_PI); + return cos_theta < cos_thresh; + }); +} + +struct ConnectionLength { + /// the distance of the longest connection each vertex involved + double max_length = 0.0; + /// the maximum length of connection before connecting handles (conservative connection) + double pre_max_length = 0.0; +}; + +/// @brief initialize the graph and related information +/// @param vertices: vertices of the component +/// @param smoothed_v: smoothed vertices of the component +/// @param normals: normals of this component +/// @param neighbor_map +/// @param connection_lengths [OUT] Distance of each vertex's longest connection and the maximum length before connecting handles +/// @param k +/// @param is_euclidean +/// @return a light-weight graph with the essential connections for building MST +SimpGraph init_graph( + const std::vector& smoothed_v, + const std::vector& normals, + const NeighborMap& neighbor_map, + std::vector& connection_lengths, + const int k, + const double cross_connection_threshold, + const bool is_euclidean) +{ + SimpGraph dist_graph; + AMGraph::NodeSet sets; + dist_graph.reserve(smoothed_v.size(), k); + //GEL_ASSERT_EQ(vertices.size(), smoothed_v.size()); + GEL_ASSERT_EQ(smoothed_v.size(), normals.size()); + + for (int i = 0; i < smoothed_v.size(); i++) { + auto node = dist_graph.add_node(); + sets.insert(node); + } + + for (NodeID id_this = 0UL; id_this < smoothed_v.size(); ++id_this) { + const Point& vertex = smoothed_v[id_this]; + const Vec3& this_normal = normals[id_this]; + auto neighbors = neighbor_map[id_this]; + + connection_lengths[id_this].pre_max_length = neighbors[static_cast(double(k) * (2.0 / 3.0))].distance; + + filter_out_cross_connection(neighbors, normals, id_this, cross_connection_threshold, is_euclidean); + //GEL_ASSERT(neighbors.size() > 1); + for (const auto& neighbor : neighbors | std::views::drop(1)) { + const auto id_other = neighbor.id; + //if (id_other <= id_this) continue; + //GEL_ASSERT_NEQ(id_other, id_this, "Vertex connects back to its own"); + + const Vec3& neighbor_normal = normals[id_other]; + const Point& neighbor_pos = smoothed_v[id_other]; + const Vec3 edge = neighbor_pos - vertex; + const auto weight = + (is_euclidean) + ? (edge.length()) + : (cal_proj_dist(edge, this_normal, neighbor_normal)); + if (weight > connection_lengths[id_this].max_length) + connection_lengths[id_this].max_length = weight; + + if (weight > connection_lengths[id_other].max_length) + connection_lengths[id_other].max_length = weight; + + if (weight < 1e-8) [[unlikely]] + std::cerr << __func__ << " [ERROR] weight: " << weight << std::endl; + + // if (dist_graph.find_edge(id_this, id_other) != SimpGraph::InvalidEdgeID) + // continue; + + dist_graph.connect_nodes(id_this, id_other, weight); + } + } + + const auto components_vec = connected_components(dist_graph.inner(), sets); + GEL_ASSERT(components_vec.size() == 1, "%d", components_vec.size()); + return dist_graph; +} + +/// @brief Find the shortest path from one vertex to another in the graph +/// @param mst: the graph +/// @param start: the source vertex +/// @param target: the target vertex +/// @return number of steps in the shortest path +int find_shortest_path(const RSGraph& mst, const NodeID start, const NodeID target) +{ + std::queue q; + std::vector dist(mst.no_nodes(), -1); // Distance from the start to each node + UnorderedSet visited; + + dist[start] = 0; + q.push(start); + + while (!q.empty()) { + const auto u = q.front(); + q.pop(); + + // If the target node is reached, stop early + if (u == target) + break; + // If the node has already been visited, skip it + if (visited.contains(u)) + continue; + + visited.insert(u); + + // Explore neighbors + for (const auto& v : mst.neighbors_lazy(u)) { + if (dist[v] == -1) { + // If the node hasn't been visited + dist[v] = dist[u] + 1; // Increment distance + q.push(v); + } + } + } + + return dist[target]; +} + +/// @brief weighted smoothing method using defined neighborhood with tangential distance weighted +/// @param pool thread pool +/// @param vertices: vertices of the point cloud +/// @param normals: normal of the point cloud +/// @param neighbors_map +/// @param smoothed_v: [OUT] vertices after smoothing +void weighted_smooth( + Util::IExecutor& pool, + const std::vector& vertices, + const std::vector& normals, + const NeighborMap& neighbors_map, + std::vector& smoothed_v) +{ + auto _debug = __func__; + auto lambda = [_debug, &normals, &vertices](const size_t idx, const Point& vertex, const NeighborArray& neighbors) { + const Vec3 normal = normals[idx]; + + double weight_sum = 0.; + double amp_sum = 0.; + double max_dist = 0.; + + struct LengthWeight { + double vert_length = 0.0; + double weight = 0.0; + }; + Util::InplaceVector length_weights; + const std::intptr_t limit = (neighbors.size() < 192) ? static_cast(neighbors.size()) : 192; + length_weights.reserve(limit); + for (const auto& neighbor: neighbors | std::views::take(192)) { + const Point neighbor_pos = vertices[neighbor.id]; + const Vec3 n2this = neighbor_pos - vertex; + if (dot(normals[neighbor.id], normal) < std::cos(30. / 180. * M_PI)) { + continue; + } + const double vertical = CGLA::dot(n2this, normal); + const double n_dist = (neighbor_pos - vertex).length(); + + const double tangential_square = n_dist * n_dist - + vertical * vertical; + const double tangential_dist = (tangential_square > 0.) ? std::sqrt(tangential_square) : 0.0; + + if (!std::isfinite(tangential_dist)) [[unlikely]] { + std::cerr << n_dist << " " << vertical << std::endl; + std::cerr << _debug << ": error" << std::endl; + } + + const double weight = -tangential_dist; + if (tangential_dist > max_dist) + max_dist = tangential_dist; + + length_weights.emplace_back(vertical, weight); + weight_sum += weight; + } + for (const auto& length_weight : length_weights) { + amp_sum += length_weight.vert_length * (length_weight.weight + max_dist); + } + weight_sum += (max_dist * static_cast(length_weights.size())); + + weight_sum = (weight_sum == 0.) ? 1. : weight_sum; + + amp_sum /= weight_sum; + if (!std::isfinite(amp_sum)) [[unlikely]] + std::cout << _debug << ": error" << std::endl; + const Vec3 move = amp_sum * normal; + return vertex + move; + }; + Util::Parallel::enumerate_map2(pool, vertices, neighbors_map, smoothed_v, lambda); +} + +auto normalize_normals(std::vector& normals) -> void +{ + for (auto& normal : normals) { + normal.normalize(); + } +} + +void estimate_normal_no_normals_memoized( + Util::IExecutor& pool, + const std::vector& vertices, + const NeighborMap& neighbors, + std::vector& normals) +{ + normals.clear(); + // Data type transfer & Cal diagonal size + auto _debug = __func__; + auto lambda = [&](const NeighborArray& neighbors_of_this) { + // need id, distance and coords anyway + + auto neighbor_coords = neighbors_of_this | std::views::transform([&](const auto& neighbor) { + return vertices[neighbor.id]; + }); + const Vec3 normal = estimateNormal(neighbor_coords); + + if (std::isnan(normal.length())) [[unlikely]] { + std::cerr << _debug << ": error" << std::endl; + std::cerr << neighbors_of_this.size() << std::endl; + } + return normal; + }; + Util::Parallel::map(pool, neighbors, normals, lambda); +} + +/// @brief Calculate cos angle weight for correcting normal orientation +/// @param this_normal: normal of current vertex +/// @param neighbor_normal: normal of its neighbor vertex +/// @return angle weight calculated +double cal_angle_based_weight(const Vec3& this_normal, const Vec3& neighbor_normal) +{ + const double dot_pdt = std::abs( + dot(this_normal, neighbor_normal) / (this_normal.length() * neighbor_normal.length())); + const double dot_pdt_clamped = std::clamp(dot_pdt, 0., 1.0); + return 1.0 - dot_pdt_clamped; +} + +/// Generic template for creating an MST +template */ typename TargetGraph, + /*std::derived_from*/ typename SourceGraph, + std::invocable NodeInserter, // TargetGraph -> NodeID -> () + std::invocable DistanceFunc, // SourceGraph -> NodeID -> NodeID -> double + std::invocable EdgeInserterFunc> // TargetGraph -> NodeID -> NodeID -> () +TargetGraph make_mst( + const SourceGraph& g, + NodeID root, + NodeInserter&& node_inserter, + DistanceFunc&& distance_function, + EdgeInserterFunc&& edge_insertion) +{ + GEL_ASSERT_NEQ(root, AMGraph::InvalidNodeID); + TargetGraph gn; + struct QElem { + double distance; + NodeID from; + NodeID to; + + bool operator>(const QElem& rhs) + { + return distance > rhs.distance; + } + }; + struct Bool { + bool inner; + }; + for (auto n : g.node_ids()) + node_inserter(gn, n); + std::vector in_tree(gn.no_nodes(), Bool(false)); + in_tree[root].inner = true; + + std::priority_queue, std::greater<>> queue; + for (auto neighbor : g.neighbors_lazy(root)) { + const auto distance = distance_function(g, neighbor, root); + queue.emplace(distance, root, neighbor); + } + + while (!queue.empty()) { + auto [distance, id1, id2] = queue.top(); + queue.pop(); + + if (!in_tree[id2].inner) { + in_tree[id2].inner = true; + + edge_insertion(gn, id1, id2); + + GEL_ASSERT(std::ranges::distance(g.neighbors_lazy(id2)) > 0); + for (auto neighbor : g.neighbors_lazy(id2) + | std::views::filter([&](auto&& nb){return !in_tree[nb].inner;})) { + const auto distance2 = distance_function(g, neighbor, id2); + queue.emplace(distance2, id2, neighbor); + } + } + } + return gn; +} + +RSGraph minimum_spanning_tree( + const SimpGraph& g, + const NodeID root, + const std::vector& normals, + const std::vector& vertices, + const bool is_euclidean) +{ + GEL_ASSERT_NEQ(root, AMGraph::InvalidNodeID); + auto gn = make_mst( + g, root, + [&](RSGraph& graph, NodeID n) { graph.add_node(vertices[n], normals[n]); }, + [&](const SimpGraph& graph, NodeID n, NodeID m) { + return CGLA::sqr_length(vertices[m] - vertices[n]); + }, + [&](RSGraph& graph, NodeID id1, NodeID id2) { + graph.add_edge(id1, id2); + } + ); + // by the definition of an mst + GEL_ASSERT_EQ(gn.m_vertices.size(), gn.m_edges.size() + 1); + return gn; +} + +SimpGraph minimum_spanning_tree(const SimpGraph& g, NodeID root) +{ + auto gn = make_mst(g, root, + [](SimpGraph& gn, NodeID n) { gn.add_node(); }, + [](const SimpGraph& g, NodeID n, NodeID m) { return g.get_weight(n, m); }, + [](SimpGraph& gn, NodeID n, NodeID m) { gn.connect_nodes(n, m); }); + return gn; +} + +/// @brief Determine the normal orientation +/// @param pool Thread pool +/// @param kdTree +/// @param in_smoothed_v +/// @param normals: [OUT] normal of the point cloud with orientation corrected +/// @param k +void correct_normal_orientation( + Util::IExecutor& pool, + const Tree& kdTree, + const std::vector& in_smoothed_v, + std::vector& normals, + const int k) +{ + /// The graph has the angles as weights + const auto [g_angle, sets] = [&] { + SimpGraph g_angle_temp; + AMGraph::NodeSet sets_temp; + // sets_temp.reserve(in_smoothed_v.size()); // TODO: flat_hash_set can be reserved + + for (int i = 0; i < in_smoothed_v.size(); i++) { + sets_temp.insert(g_angle_temp.add_node()); + } + + const auto all_neighbors = calculate_neighbors(pool, in_smoothed_v, kdTree, k); + + // Init angle based graph + for (int i = 0; i < in_smoothed_v.size(); i++) { + const auto& neighbors = all_neighbors[i]; + const auto& this_normal = normals[i]; + + for (const auto neighbor : neighbors + | std::views::drop(1) + | std::views::filter([&i, &g_angle_temp](auto&& nb) + {return g_angle_temp.find_edge(i, nb.id) == AMGraph::InvalidEdgeID;}) + ) { + + //if (g_angle_temp.find_edge(i, neighbor.id) != AMGraph::InvalidEdgeID) + // continue; + const auto& neighbor_normal = normals[neighbor.id]; + const double angle_weight = cal_angle_based_weight(this_normal, neighbor_normal); + + g_angle_temp.connect_nodes(i, neighbor.id, angle_weight); + } + } + + return std::make_tuple(g_angle_temp, sets_temp); + }(); + + const auto components_vec = connected_components(g_angle.inner(), sets); + + // The number of components and their relative sizes is inconsistent, don't parallelize this + for (const auto& i : components_vec) { + NodeID root = *i.begin(); + SimpGraph mst_angle = minimum_spanning_tree(g_angle, root); + + /// A trivial wrapper over bool to avoid the std::vector specialization + struct Boolean { + bool inner; + }; + auto visited_vertex = std::vector(g_angle.no_nodes(), Boolean{false}); + + // This uses the MST to visit every node + // Start from the root + std::queue to_visit; + to_visit.push(root); + while (!to_visit.empty()) { + const NodeID node_id = to_visit.front(); + to_visit.pop(); + + visited_vertex[node_id].inner = true; + const Vec3 this_normal = normals[node_id]; + for (auto vd : mst_angle.neighbors_lazy(node_id)) { + if (!visited_vertex[vd].inner) { + to_visit.push(vd); + const Vec3 neighbor_normal = normals[vd]; + if (dot(this_normal, neighbor_normal) < 0) { + normals[vd] = -normals[vd]; + } + } + } + } + } +} + +void init_face_loop_label(RSGraph& g) +{ + constexpr NodeID start_v = 0; + NodeID last_vertex = start_v; + auto loop_step = 0UL; + // TODO: unsafe access + NodeID current_vertex = g.m_neighbors[start_v].begin()->v; + do { + auto& next_neighbor = g.predecessor(current_vertex, last_vertex); + + next_neighbor.tree_id = g.etf.accumulate(); + + last_vertex = current_vertex; + current_vertex = next_neighbor.v; + + loop_step++; + } while (current_vertex != g.m_neighbors[start_v].begin()->v || last_vertex != start_v); + + std::cout << "Loop step initialization finished after " + std::to_string(loop_step) + " steps." << std::endl; +} + +/// @brief Project a vector to a plane +/// @param input: Vector to be projected +/// @param normal: normal to the plane +/// @return projected Vector +Vec3 projected_vector(const Vec3& input, const Vec3& normal) +{ + const auto normal_normed = CGLA::normalize(normal); + return input - CGLA::dot(input, normal_normed) * normal_normed; + //return input - dot(input, normal) * normal; +} + +/// @brief Check if two segments are intersecting on both planes (defined by their normal) they belong +/// @param mst: graph and vertex information +/// @param v1: 1st vertex of segment 1 +/// @param v2: 2nd vertex of segment 1 +/// @param v3: 1st vertex of segment 2 +/// @param v4: 2nd vertex of segment 2 +/// @return if they are intersecting with each other +bool is_intersecting(const RSGraph& mst, const NodeID v1, const NodeID v2, const NodeID v3, const NodeID v4) +{ + const auto& p1 = mst.m_vertices[v1].coords; + const auto& p2 = mst.m_vertices[v2].coords; + const auto& n1 = mst.m_vertices[v1].normal; + const auto& n2 = mst.m_vertices[v2].normal; + const Vec3 normal_12 = (n1 + n2) / 2.; + + const auto& p3 = mst.m_vertices[v3].coords; + const auto& p4 = mst.m_vertices[v4].coords; + const auto& n3 = mst.m_vertices[v3].normal; + const auto& n4 = mst.m_vertices[v4].normal; + const Vec3 normal_34 = (n3 + n4) / 2.; + + auto check_intersection = [](auto&& p1, auto&& p2, auto&& p3, auto&& p4, auto&& normal) -> bool { + const Vec3 midpoint = p1 + (p2 - p1) / 2.0; + const Vec3 edge1 = p1 - midpoint; + const Vec3 edge2 = p3 - midpoint; + const Vec3 edge3 = p4 - midpoint; + const Vec3 proj_edge1 = projected_vector(edge1, normal); + const Vec3 proj_edge2 = projected_vector(edge2, normal); + const Vec3 proj_edge3 = projected_vector(edge3, normal); + const Vec3 pro1 = CGLA::cross(proj_edge2, proj_edge1); + const Vec3 pro2 = CGLA::cross(proj_edge3, proj_edge1); + return CGLA::dot(pro1, pro2) <= 0; + }; + + // On the plane of edge 12 + if (check_intersection(p1, p2, p3, p4, normal_12) && + check_intersection(p3, p4, p1, p2, normal_12)) + return true; + + // On the plane of edge 34 + if (check_intersection(p1, p2, p3, p4, normal_34) && + check_intersection(p3, p4, p1, p2, normal_34)) + return true; + + return false; +} + +/// @brief Geometry check for connection +/// @param mst: graph and vertex information +/// @param candidate: the edge to be checked +/// @param kd_tree: kd-tree for knn query +/// @return if the candidate pass the check +bool geometry_check( + const RSGraph& mst, + const TEdge& candidate, + const Tree& kd_tree, + std::vector>& neighbors) +{ + const NodeID& v1 = candidate.from; + const NodeID& v2 = candidate.to; + const Point& p1 = mst.m_vertices[v1].coords; + const Point& p2 = mst.m_vertices[v2].coords; + const Vec3& n1 = mst.m_vertices[v1].normal; + const Vec3& n2 = mst.m_vertices[v2].normal; + + const Vec3 mean_normal = (n1 + n2) * 0.5; + + const Point search_center = (p1 + p2) * 0.5; + const double radius = (p2 - p1).length() * 0.5; + + neighbors.clear(); + kd_tree.in_sphere(search_center, radius * 3.0, neighbors); + + auto in_rejection_set = [&](NodeID neighbor) -> bool { + return + (neighbor != v1 && + neighbor != v2 && + CGLA::dot(mst.m_vertices[neighbor].normal, mean_normal) > 0.5); // std::cos(60. / 180. * M_PI)); + }; + for (const auto neighbor : neighbors + | std::views::values + | std::views::filter(in_rejection_set)) { + + if (std::ranges::any_of( + mst.m_neighbors[neighbor] | + std::views::transform([](auto&& x) { return x.v; }) | + std::views::filter(in_rejection_set), [&](const auto& rej_neighbor_neighbor) { + return is_intersecting(mst, v1, v2, neighbor, rej_neighbor_neighbor); + } + )) return false; + } + return true; +} + +bool geometry_check( + const RSGraph& mst, + const TEdge& candidate, + const Tree& kd_tree) +{ + std::vector> neighbors; + return geometry_check(mst, candidate, kd_tree, neighbors); +} + +bool topology_check(const RSGraph& mst, const TEdge& candidate) +{ + const auto [this_v, neighbor] = candidate; + + // Topology check + const auto this_v_tree = mst.predecessor(this_v, neighbor).tree_id; + const auto neighbor_tree = mst.predecessor(neighbor, this_v).tree_id; + + if (!mst.etf.connected(this_v_tree, neighbor_tree)) { + return false; + } else { + return true; + } +} + +bool vanilla_check( + const RSGraph& mst, + const TEdge& candidate, + const Tree& kd_tree, + std::vector>& neighbors) +{ + const auto [this_v, neighbor] = candidate; + + // Topology check + const auto this_v_tree = mst.predecessor(this_v, neighbor).tree_id; + const auto neighbor_tree = mst.predecessor(neighbor, this_v).tree_id; + + if (!mst.etf.connected(this_v_tree, neighbor_tree)) { + return false; + } + + return geometry_check(mst, candidate, kd_tree, neighbors); +} + +bool routine_check(const RSGraph& mst, const FaceType& triangle) +{ + const Point& p1 = mst.m_vertices[triangle[0]].coords; + const Point& p2 = mst.m_vertices[triangle[1]].coords; + const Point& p3 = mst.m_vertices[triangle[2]].coords; + const auto len_ui = (p1 - p2).length(); + const auto len_wi = (p3 - p2).length(); + const auto len_uw = (p1 - p3).length(); + + auto max_value = std::acos(std::clamp( + dot((p3 - p2), (p1 - p2)) / len_ui / + len_wi, -1, 1)); + auto radian = std::acos(std::clamp( + dot((p2 - p1), (p3 - p1)) / len_ui / + len_uw, -1, 1)); + if (radian > max_value) + max_value = radian; + radian = std::acos(std::clamp( + dot((p1 - p3), (p2 - p3)) / len_uw / + len_wi, -1, 1)); + if (radian > max_value) + max_value = radian; + + if (max_value > 175. / 180. * M_PI || + mst.find_edge(triangle[0], triangle[2]).value().ref_time == 2 || + mst.find_edge(triangle[1], triangle[2]).value().ref_time == 2) + return true; + else + return false; +} + +void add_face(RSGraph& graph, const FaceType& item, + std::vector& faces) +{ + const auto& v_i = item[0]; + const auto& v_u = item[1]; + const auto& v_w = item[2]; + + graph.increment_ref_time(v_i, v_w); + graph.increment_ref_time(v_u, v_i); + graph.increment_ref_time(v_w, v_u); + faces.push_back(v_i); + faces.push_back(v_u); + faces.push_back(v_w); +} + +struct RegisterFaceResult { + Util::InplaceVector faces; +}; +auto register_face(const RSGraph& mst, const NodeID v1, const NodeID v2) -> std::optional +{ + GEL_ASSERT_EQ(mst.find_edge(v1, v2), RSGraph::InvalidEdgeID); + auto shared_neighbors = mst.shared_neighbors_lazy(v1, v2); + if (shared_neighbors.empty()) { + return RegisterFaceResult(); // true + } + + const Point& p1 = mst.m_vertices[v1].coords; + const Point& p2 = mst.m_vertices[v2].coords; + + const auto possible_root1 = mst.predecessor(v1, v2).v; + const auto angle1 = cal_radians_3d(p1 - mst.m_vertices[possible_root1].coords, + mst.m_vertices[possible_root1].normal, + p2 - mst.m_vertices[possible_root1].coords); + const auto possible_root2 = mst.predecessor(v2, v1).v; + const auto angle2 = cal_radians_3d(p2 - mst.m_vertices[possible_root2].coords, + mst.m_vertices[possible_root2].normal, + p1 - mst.m_vertices[possible_root2].coords); + + Util::InplaceVector temp; + for (const auto v3 : shared_neighbors) { + FaceType triangle{v1, v2, v3}; + if (v3 == possible_root1 && angle1 < M_PI) { + if (routine_check(mst, triangle) || + mst.successor(v2, v1).v != v3 || + mst.successor(possible_root1, v2).v != v1) { + return std::nullopt; // false + } + temp.emplace_back(FaceType{v1, v3, v2}); + } + + if (v3 == possible_root2 && angle2 < M_PI) { + if (routine_check(mst, triangle) || + mst.successor(v1, v2).v != v3 || + mst.successor(possible_root2, v1).v != v2) { + return std::nullopt; // false + } + temp.emplace_back(FaceType{v1, v2, v3}); + } + } + + if (temp.empty()) + return std::nullopt; // false + + return RegisterFaceResult { + temp, + }; // true +} + +/// @brief Connect handle to raise the genus number +/// @param smoothed_v: smoothed vertices of the point cloud +/// @param mst: graph and vertex information +/// @param kd_tree: kd-tree for knn query +/// @param connected_handle_root: [OUT] log the connected handles +/// @param k: number of kNN search +/// @param isEuclidean: if to use Euclidean distance +/// @param expected_genus expected genus of the mesh +/// @param step_thresh: step threshold for shortest distance path early stop +void connect_handle( + const std::vector& smoothed_v, + const Tree& kd_tree, + RSGraph& mst, + std::vector& connected_handle_root, + const int k, + const int step_thresh, + const bool isEuclidean, + const int expected_genus) +{ + std::vector imp_node; + size_t num = 0; + size_t edge_num = 0; + // Collect vertices w/ an open angle larger than pi + for (int i = 0; i < mst.no_nodes(); i++) { + const auto& neighbors = mst.m_neighbors[i]; + // FIXME: Clean this up + float last_angle = (*(--neighbors.end())).angle; + float this_angle; + + for (auto& neighbor : neighbors) { + this_angle = neighbor.angle; + float angle_diff = this_angle - last_angle; + if (angle_diff < 0) + angle_diff += 2 * M_PI; + if (angle_diff > M_PI) + imp_node.push_back(i); + last_angle = this_angle; + } + //for (const auto [neighbor_old, neighbor_next] : slide2_wraparound(neighbors)) { + // auto last_angle = neighbor_old.angle; + // auto this_angle = neighbor_next.angle; + // auto angle_diff = this_angle - last_angle; + // if (angle_diff < 0) + // angle_diff += 2 * M_PI; + // if (angle_diff > M_PI) + // imp_node.push_back(i); + //} + } + + struct Connection { + NodeID p_from; + NodeID p_to; + uint tree_from; + uint tree_to; + }; + std::vector connect; + + ThreadPool pool; + const auto neighbors_map = calculate_neighbors(pool, smoothed_v, imp_node, kd_tree, k); + + GEL_ASSERT_EQ(imp_node.size(), neighbors_map.size()); + for (auto i = 0UL; i < imp_node.size(); i++) { + const auto& this_v = imp_node[i]; + const auto& neighbors = neighbors_map[i]; + + // Potential handle collection + for (auto& neighbor: neighbors | std::views::drop(1)) { + + if (mst.find_edge(this_v, neighbor.id) != RSGraph::InvalidEdgeID) + continue; + const auto tree = mst.etf.representative(mst.predecessor(this_v, neighbor.id).tree_id); + const auto to_tree = mst.etf.representative(mst.predecessor(neighbor.id, this_v).tree_id); + const TEdge candidate(this_v, neighbor.id); + if (tree != to_tree && geometry_check(mst, candidate, kd_tree)) { + // TODO: Check if any tree shares root, and return corresponding edges + connect.emplace_back(this_v, neighbor.id, tree, to_tree); + break; + } + } + } + std::cout << "connections: " << connect.size() << "\n"; + + // Select one handle + Map, FaceConnectionKeyHasher> face_connections; + for (int i = 0; i < connect.size(); i++) { + uint tree = connect[i].tree_from; //tree_id[i]; + uint to_tree = connect[i].tree_to; //to_tree_id[i]; + if (to_tree > tree) + std::swap(tree, to_tree); + const auto key = FaceConnectionKey{tree, to_tree}; + if (!face_connections.contains(key)) + face_connections[key] = std::vector{i}; + else { + face_connections[key].push_back(i); + } + } + + // Sort + std::vector sorted_face; + for (const auto& key : face_connections | std::views::keys) { + auto length = face_connections[key].size(); + sorted_face.emplace_back(length, key); + } + std::ranges::sort(sorted_face, face_comparator); + for (const auto& val : sorted_face | std::views::values) { + const std::vector& idx_vec = face_connections[val]; + if (idx_vec.size() <= 5 || (expected_genus >= 0 && num >= expected_genus)) + break; + + bool isFind = false; + for (const int idx : idx_vec) { + const NodeID& this_v = connect[idx].p_from; + const Point& query = mst.m_vertices[this_v].coords; + const NodeID& connected_neighbor = connect[idx].p_to; + // TODO: path and step_thresh are both dead + const int steps = find_shortest_path(mst, this_v, connected_neighbor); + if (steps > step_thresh) { + isFind = true; + const auto candidate = TEdge(this_v, connected_neighbor); + if (geometry_check(mst, candidate, kd_tree)) { + + mst.add_edge(this_v, connected_neighbor); + connected_handle_root.push_back(this_v); + connected_handle_root.push_back(connected_neighbor); + + edge_num++; + } + } + } + if (isFind) { + num++; + } + } + + std::cout << "Handle Connection done :)" << std::endl; + std::cout << std::to_string(num) << " pairs of faces are connected." << std::endl; + std::cout << std::to_string(edge_num) << " edges are connected." << std::endl; +} + +bool explore( + const RSGraph& G, + const NodeID i, + FacePriorityQueue& queue, + const std::vector& length_thresh, + const bool is_euclidean) +{ + const NodeID v_i = i; + bool isFound = false; + for (const auto& neighbor : G.m_neighbors[i]) { + const NodeID v_u = neighbor.v; + const NodeID v_w = G.successor(i, v_u).v; + + const Point& w_pos = G.m_vertices[v_w].coords; + const Point& u_pos = G.m_vertices[v_u].coords; + const Point& i_pos = G.m_vertices[v_i].coords; + const Vec3& i_normal = G.m_vertices[v_i].normal; + const Vec3& u_normal = G.m_vertices[v_u].normal; + const Vec3& w_normal = G.m_vertices[v_w].normal; + const auto angle = cal_radians_3d(w_pos - i_pos, i_normal, + u_pos - i_pos); + const bool isLargerThanPi = angle < M_PI; + const FaceType face_vector{v_i, v_u, v_w}; + if (v_u != v_w && + isLargerThanPi && + G.find_edge(v_u, v_w) == RSGraph::InvalidEdgeID) { + const auto score = (is_euclidean) + ? (G.m_vertices[v_u].coords - G.m_vertices[v_w].coords).length() + : cal_proj_dist(G.m_vertices[v_u].coords - G.m_vertices[v_w].coords, + u_normal, w_normal); + if (score > length_thresh[v_u].max_length || score > length_thresh[v_w].max_length) + continue; + else if (score >= 0) { + queue.emplace(face_vector, score); + isFound = true; + } + } + } + + return isFound; +} + +bool check_branch_validity(const RSGraph& G, const NodeID root, const NodeID branch1, const NodeID branch2) +{ + constexpr auto angle_thresh = 0. / 180. * M_PI; + + const Point& pos_u = G.m_vertices[branch1].coords; + const Point& pos_w = G.m_vertices[branch2].coords; + const Vec3& normal_u = G.m_vertices[branch1].normal; + const Vec3& normal_w = G.m_vertices[branch2].normal; + + const auto is_valid = [&root](auto this_radian, auto former, auto next) { + if (next.v == root) { + auto diff = next.angle - this_radian; + if (diff < 0) + diff += 2 * M_PI; + if (diff < M_PI) + return true; + } + if (former.v == root) { + auto diff = -former.angle + this_radian; + if (diff < 0) + diff += 2 * M_PI; + if (diff < M_PI) + return true; + } + return false; + }; + + // Check u's RS validity + { + const auto this_radian = cal_radians_3d(pos_w - pos_u, normal_u); + const auto& former = G.predecessor(branch1, branch2); + const auto& next = G.successor(branch1, branch2); + if (!is_valid(this_radian, former, next)) + return false; + + // Thresh on angle + auto diff_angle_thresh = this_radian - former.angle; + if (diff_angle_thresh < 0) + diff_angle_thresh += M_PI * 2.; + if (diff_angle_thresh < angle_thresh) + return false; + } + + // Check w's RS validity + { + const auto this_radian = cal_radians_3d(pos_u - pos_w, normal_w); + const auto& former = G.predecessor(branch2, branch1); + const auto& next = G.successor(branch2, branch1); + if (!is_valid(this_radian, former, next)) + return false; + + // Thresh on angle + if (const auto diff_angle_thresh2 = -this_radian + next.angle + 2. * M_PI; + diff_angle_thresh2 < angle_thresh) + return false; + } + + return true; +} + + +// TODO: validity of what? +bool check_validity( + const RSGraph& graph, + const std::pair& item) +{ + const NodeID v_i = item.first[0]; + const NodeID v_u = item.first[1]; + const NodeID v_w = item.first[2]; + const Point& pos_i = graph.m_vertices[v_i].coords; + const Point& pos_u = graph.m_vertices[v_u].coords; + const Point& pos_w = graph.m_vertices[v_w].coords; + const Vec3& normal_i = graph.m_vertices[v_i].normal; + + if (graph.find_edge(v_u, v_w) != RSGraph::InvalidEdgeID) + return false; + + // Non-manifold edge check + if (graph.find_edge(v_i, v_u).value().ref_time == 2 || + graph.find_edge(v_i, v_w).value().ref_time == 2) + return false; + + // Check this rotation system + if (graph.successor(v_i, v_u).v != v_w) + return false; + const auto angle = cal_radians_3d(pos_w - pos_i, normal_i, pos_u - pos_i); + if (angle > M_PI) + return false; + + // Check the rotation system's validity of branch nodes + if (!check_branch_validity(graph, v_i, v_u, v_w)) { + return false; + } + + return true; +} + +void triangulate( + std::vector& faces, + RSGraph& graph, + const bool is_euclidean, + const std::vector& length_thresh, + const std::vector& connected_handle_root) +{ + // Since we are clearing every element here, a TreeSet has much better performance than a Swiss table + OrderedSet to_visit; + FacePriorityQueue queue; + + // Init priority queue + for (auto i = 0UL; i < graph.no_nodes(); i++) { + to_visit.insert(i); + } + + for (const auto i : connected_handle_root) { + explore(graph, i, queue, length_thresh, is_euclidean); + to_visit.erase(i); + } + + std::cout << "Global init done :)" << std::endl; + + while (!to_visit.empty()) { + while (!queue.empty()) { + std::pair item = queue.top(); + queue.pop(); + + if (item.second >= 0.0 && !check_validity(graph, item)) { + continue; + } + + // Add the edge + const NodeID v_i = item.first[0]; + const NodeID v_u = item.first[1]; + const NodeID v_w = item.first[2]; + + if (graph.find_edge(v_u, v_w) == RSGraph::InvalidEdgeID) { + graph.add_edge(v_u, v_w); + add_face(graph, item.first, faces); + } else { + continue; + } + + // Deal with incident triangles + for (const NodeID incident_root : graph.shared_neighbors_lazy(v_u, v_w)) { + if (incident_root == v_i) + continue; + std::array face{incident_root, v_w, v_u}; + + // Non-manifold edge check + if (graph.find_edge(incident_root, v_u).value().ref_time == 2 || + graph.find_edge(incident_root, v_w).value().ref_time == 2 || + graph.find_edge(v_u, v_w).value().ref_time == 2) + continue; + + add_face(graph, face, faces); + } + + to_visit.erase(v_u); + to_visit.erase(v_w); + + // Explore and sanity check + explore(graph, v_u, queue, length_thresh, is_euclidean); + explore(graph, v_w, queue, length_thresh, is_euclidean); + } + + if (!to_visit.empty()) { + NodeID pick = *to_visit.begin(); + to_visit.erase(pick); + explore(graph, pick, queue, length_thresh, is_euclidean); + } + } +} + +/// @brief Build minimum spanning tree (MST) +/// @param out_mst: [OUT] constructed MST +/// @param g: connection information of the mst +/// @param root root node +/// @param is_euclidean: if to use Euclidean distance +/// @param vertices: coordinates of the point cloud +/// @param normals: normal of the point cloud +void build_mst( + const SimpGraph& g, + const NodeID root, + RSGraph& out_mst, + const std::vector& normals, + const std::vector& vertices, + const bool is_euclidean) +{ + RSGraph temp = minimum_spanning_tree(g, root, normals, vertices, is_euclidean); + + // Fix strong ambiguous points + if (!is_euclidean) { + for (int i = 0; i < temp.m_edges.size(); i++) { + const NodeID& source = temp.m_edges[i].source; + const NodeID& target = temp.m_edges[i].target; + const Vec3& normal1 = temp.m_vertices[source].normal; + const Vec3& normal2 = temp.m_vertices[target].normal; + const Point& pos1 = temp.m_vertices[source].coords; + const Point& pos2 = temp.m_vertices[target].coords; + if (temp.valence(source) >= 2 && temp.valence(target) >= 2) + continue; + const Vec3 edge = pos2 - pos1; + + const Vec3 normal_sum = (normal1 + normal2) * 0.5; + const auto cos_angle = std::abs(dot(edge, normal_sum / edge.length())); + if (cos_angle > std::cos(10. / 180. * M_PI)) { + NodeID parent; + if (temp.valence(source) == 1) { + temp.m_vertices[source].normal = temp.m_vertices[target].normal; + parent = target; + } else { + temp.m_vertices[target].normal = temp.m_vertices[source].normal; + parent = source; + } + + for (const auto neighbor : temp.neighbors_lazy(parent)) { + if (temp.m_vertices[neighbor].normal_rep == Vertex::InvalidNormalRep) { + // this conversion is fine since we need 10^18 vertices for it to matter + temp.m_vertices[neighbor].normal_rep = parent; + } else { + // Collision! + temp.m_vertices[neighbor].normal_rep = Vertex::CollisionRep; + } + } + } + } + for (int i = 0; i < temp.m_vertices.size(); i++) { + if (temp.m_vertices[i].normal_rep >= 0) + temp.m_vertices[i].normal = temp.m_vertices[temp.m_vertices[i].normal_rep].normal; + } + } + + // Build corrected MST + size_t verts = 0; + for (const auto& vertex : temp.m_vertices) { + out_mst.add_node(vertex.coords, vertex.normal); + verts++; + } + + size_t edges = 0; + for (const auto& m_edge : temp.m_edges) { + out_mst.add_edge(m_edge.source, m_edge.target); + edges++; + } + GEL_ASSERT_EQ(verts, edges + 1); +} + + +auto estimate_normals_included_normals( + const std::vector& vertices, + std::vector& normals, + const Distance dist) -> std::vector +{ + GEL_ASSERT_EQ(vertices.size(), normals.size()); + ThreadPool pool; + std::vector smoothed_v; + std::vector temp; + const int smoothing_size = std::max(static_cast(static_cast(vertices.size()) / 2000.), 192); + + normalize_normals(normals); + if (dist == Distance::Euclidean) { + smoothed_v = vertices; + } else if (dist == Distance::Tangent) { + const auto indices = std::ranges::iota_view(0UL, vertices.size()); + const auto kdTree = build_kd_tree_of_indices(vertices, indices); + const auto neighbors = + calculate_neighbors(pool, vertices, kdTree, smoothing_size); + weighted_smooth(pool, vertices, normals, neighbors, smoothed_v); + + + temp.reserve(smoothed_v.size()); + std::swap(temp, smoothed_v); + smoothed_v.clear(); + + weighted_smooth(pool, temp, normals, neighbors, smoothed_v); + } else { + GEL_ASSERT(false, "unreachable"); + } + GEL_ASSERT_EQ(vertices.size(), normals.size()); + return smoothed_v; +} + +auto estimate_normals_no_normals( + const std::vector& vertices, + std::vector& normals, + const Distance dist) -> std::vector +{ + GEL_ASSERT_EQ(normals.size(), 0LU); + ThreadPool pool; + const int smoothing_size = std::max(static_cast(static_cast(vertices.size()) / 2000.), 192); + + const auto indices = std::ranges::iota_view(0UL, vertices.size()); + const auto kdTree = build_kd_tree_of_indices(vertices, indices); + auto neighbors = calculate_neighbors(pool, vertices, kdTree, smoothing_size); + + estimate_normal_no_normals_memoized(pool, vertices, neighbors, normals); + + std::vector smoothed_v; + + switch (dist) { + case Distance::Euclidean: + smoothed_v = vertices; + break; + case Distance::Tangent: + std::cout << "Smoothing round 1 ..." << std::endl; + weighted_smooth(pool, vertices, normals, neighbors, smoothed_v); + break; + default: + GEL_ASSERT(false, "unreachable"); + } + + const auto temp_tree1 = build_kd_tree_of_indices(smoothed_v, indices); + neighbors = calculate_neighbors(pool, smoothed_v, temp_tree1, smoothing_size); + estimate_normal_no_normals_memoized(pool, smoothed_v, neighbors, normals); + + if (dist == Distance::Tangent) { + std::vector temp; + temp.reserve(smoothed_v.size()); + std::swap(temp, smoothed_v); + smoothed_v.clear(); + std::cout << "Smoothing round 2 ..." << std::endl; + weighted_smooth(pool, temp, normals, neighbors, smoothed_v); + + const Tree temp_tree2 = build_kd_tree_of_indices(smoothed_v, indices); + neighbors = calculate_neighbors(pool, smoothed_v, temp_tree2, smoothing_size, std::move(neighbors)); + estimate_normal_no_normals_memoized(pool, smoothed_v, neighbors, normals); + } + GEL_ASSERT_EQ(vertices.size(), normals.size()); + return smoothed_v; +} + +[[nodiscard]] +auto estimate_normals_and_smooth( + Util::IExecutor& pool, + const std::vector& org_vertices, + std::vector& org_normals, + Distance dist) -> std::vector +{ + if (org_normals.empty()) { + return estimate_normals_no_normals(org_vertices, org_normals, dist); + } else { + return estimate_normals_included_normals(org_vertices, org_normals, dist); + } +} + +struct Components { + std::vector> vertices; + std::vector> smoothed_v; + std::vector> normals; +}; + +/// @brief Find the number of connected components and separate them +/// @param pool: Thread pool to use +/// @param vertices: vertices of the point cloud +/// @param smoothed_v: smoothed vertices of the point cloud +/// @param normals: normal of the point cloud vertices +/// @param neighbor_map +/// @param opts: theta: (cross-connection threshold) angle threshold to avoid connecting vertices on different surface +/// r: (outlier_thresh) threshold distance(?) to remove an outlier +/// k +/// isEuclidean +/// @return Split components +[[nodiscard]] +auto split_components( + Util::IExecutor& pool, + const NeighborMap& neighbor_map, + std::vector&& vertices, + std::vector&& normals, + std::vector&& smoothed_v, + const RsROpts& opts) + -> Components +{ + GEL_ASSERT_EQ(vertices.size(), normals.size()); + GEL_ASSERT_EQ(vertices.size(), smoothed_v.size()); + std::vector> component_vertices; + std::vector> component_smoothed_v; + std::vector> component_normals; + + // Identifies clusters of vertices which are reconstructed to disparate meshes + const double outlier_thresh = opts.r; + double total_edge_length = 0; + AMGraph::NodeSet sets; + SimpGraph components; + for (int i = 0; i < vertices.size(); i++) { + sets.insert(components.add_node()); + } + + // Construct graph + for (const auto& neighbors : neighbor_map) { + const NodeID this_idx = neighbors[0].id; + for (const auto& neighbor : neighbors | std::views::drop(1)) { + const NodeID idx = neighbor.id; + if (this_idx < idx) continue; + const double length = neighbor.distance; + + total_edge_length += length; + + components.connect_nodes(this_idx, idx); + } + } + const double thresh_r = (total_edge_length * outlier_thresh) / static_cast(components.no_edges()); + // Remove Edges Longer than the threshold + std::vector> edge_rm_v; + for (NodeID vertex1 = 0; vertex1 < components.no_nodes(); ++vertex1) { + for (const auto& vertex2 : components.neighbors_lazy(vertex1)) { + const double edge_length = (vertices[vertex1] - vertices[vertex2]).length(); + + if (edge_length > thresh_r) { + edge_rm_v.emplace_back(vertex1, vertex2); + } + } + } + for (auto& [fst, snd] : edge_rm_v) { + components.disconnect_nodes(fst, snd); + } + // Find Components + const auto components_vec = connected_components(components.inner(), sets); + std::cout << "The input contains " << components_vec.size() << " connected components." << std::endl; + // Valid Components and create new vectors for components + const auto threshold = std::min(vertices.size(), 100); + for (auto& component : components_vec) { + if (component.size() >= threshold) { + std::vector this_normals; + this_normals.reserve(component.size()); + std::vector this_vertices; + this_vertices.reserve(component.size()); + std::vector this_smoothed_v; + this_smoothed_v.reserve(component.size()); + for (const auto& element : component) { + this_vertices.push_back(vertices[element]); + this_smoothed_v.push_back(smoothed_v[element]); + this_normals.push_back(normals[element]); + } + component_normals.emplace_back(std::move(this_normals)); + component_vertices.emplace_back(std::move(this_vertices)); + component_smoothed_v.emplace_back(std::move(this_smoothed_v)); + } + } + std::cout << component_vertices.size() << " of them will be reconstructed." << std::endl; + + return Components( + std::move(component_vertices), + std::move(component_smoothed_v), + std::move(component_normals) + ); +} + +void export_edges(const RSGraph& g, const std::string& out_path) { + std::ofstream file(out_path); + // Write vertices + file << "# List of geometric vertices" << std::endl; + for (int i = 0; i < g.m_vertices.size(); i++) { + Point this_coords = g.m_vertices[i].coords; + file << "v " << std::to_string(this_coords[0]) + << " " << std::to_string(this_coords[1]) + << " " << std::to_string(this_coords[2]) << "\n"; + } + + // Write lines + file << std::endl; + file << "# Line element" << std::endl; + for (int i = 0; i < g.m_vertices.size(); i += 2) + file << "l " << i + 1 + << " " << i + 2 << "\n"; + //file.close(); +} + +void export_graph(const RSGraph& g, const std::string& out_path) { + std::ofstream file(out_path); + // Write vertices + file << "# List of geometric vertices\n"; + for (int i = 0; i < g.m_vertices.size(); i++) { + Point this_coords = g.m_vertices[i].coords; + file << "v " << std::to_string(this_coords[0]) + << " " << std::to_string(this_coords[1]) + << " " << std::to_string(this_coords[2]) << "\n"; + } + + // Write lines + file << "\n# Line elements\n"; + for (int i = 0; i < g.m_edges.size(); ++i) { + auto edge = g.m_edges[i]; + file << "l " << (edge.source + 1) << " " << (edge.target + 1) << "\n"; + } +} + +bool vec3_eq_(const Vec3& lhs, const Vec3& rhs, double eps = 1e-4) +{ + return lhs.all_le(rhs + Vec3(eps)) && lhs.all_ge(rhs - Vec3(eps)); +} + + +RSGraph from_simp_graph(const SimpGraph& graph, const std::vector& points) +{ + RSGraph copy; + + for (auto id: graph.node_ids()) { + copy.add_node(points.at(id)); + } + for (auto id: graph.node_ids()) { + for (auto neighbor: graph.neighbors_lazy(id)) { + if (id < neighbor) { + copy.add_edge(id, neighbor); + } + } + } + return copy; +} + +auto component_to_manifold( + Util::IExecutor& pool, + const RsROpts& opts, + const std::vector& vertices, + const std::vector& normals, + const std::vector& smoothed_v, + const Tree& kd_tree, + const NeighborMap& neighbor_map +) -> Manifold +{ + Util::RSRTimer inner_timer; + std::cout << "Init mst" << std::endl; + // Initial Structure + RSGraph mst; + mst.reserve(vertices.size(), opts.k); + + std::vector edge_length; + std::vector connection_lengths(vertices.size(), ConnectionLength()); + { + inner_timer.start("init_graph"); + SimpGraph g = init_graph(smoothed_v, normals, neighbor_map, connection_lengths, opts.k, opts.theta, + opts.dist == Distance::Euclidean); + inner_timer.end("init_graph"); + // Generate MST + inner_timer.start("build_mst"); + + build_mst(g, 0, mst, normals, smoothed_v, opts.dist == Distance::Euclidean); + //export_graph(from_simp_graph(g, vertices), "all_connected.obj"); + //export_graph(mst, "mst.obj"); + inner_timer.end("build_mst"); + + // Edge arrays and sort + inner_timer.start("edge_length"); + for (NodeID node : g.node_ids()) { + for (NodeID node_neighbor : g.neighbors_lazy(node)) { + if (node < node_neighbor) { + const Vec3 edge = smoothed_v[node] - smoothed_v[node_neighbor]; + const double len = (opts.dist == Distance::Euclidean) + ? edge.length() + : cal_proj_dist(edge, normals[node], normals[node_neighbor]); + + if (len > connection_lengths[node].pre_max_length || + len > connection_lengths[node_neighbor].pre_max_length) + continue; + edge_length.emplace_back(TEdge(node, node_neighbor), len); + } + } + } + inner_timer.end("edge_length"); + + inner_timer.start("sort"); + std::ranges::sort(edge_length, edge_comparator); + inner_timer.end("sort"); + } + + // Initialize face loop label + mst.etf.reserve(6 * vertices.size()); + init_face_loop_label(mst); + + std::vector flattened_face; + flattened_face.reserve(vertices.size() * 2 * 3); // A fairly reasonable heuristic + + std::vector> neighbors; + inner_timer.start("edge_connection"); + for (auto& [this_edge, edge_len] : edge_length) { + if (mst.find_edge(this_edge.from, this_edge.to) == RSGraph::InvalidEdgeID && + vanilla_check(mst, this_edge, kd_tree, neighbors)) { + auto result = register_face(mst, this_edge.from, this_edge.to); + if (result.has_value()) { + mst.add_edge(this_edge.from, this_edge.to); + mst.maintain_face_loop(this_edge.from, this_edge.to); + for (const auto& face : result->faces) { + add_face(mst, face, flattened_face); + } + } + } + } + inner_timer.end("edge_connection"); + std::cout << "edge length length: " << edge_length.size() << "\n"; + + // Create handles & Triangulation + inner_timer.start("triangulation"); + if (opts.genus != 0) { + std::vector connected_handle_root; + connect_handle(smoothed_v, kd_tree, mst, connected_handle_root, opts.k, opts.n,opts.dist == Distance::Euclidean, opts.genus); + triangulate(flattened_face, mst, opts.dist == Distance::Euclidean, connection_lengths, connected_handle_root); + } + inner_timer.end("triangulation"); + + inner_timer.start("build_manifold"); + Manifold res; + + build_manifold(res, vertices, flattened_face, 3); + inner_timer.end("build_manifold"); + + std::cout << "\n"; + inner_timer.show(); + std::cout << "\n"; + + return res; +} + +auto point_cloud_to_mesh_impl( + std::vector&& vertices_copy, + std::vector&& normals_copy, + Util::RSRTimer& timer, + ThreadPool& pool, + const RsROpts& opts) -> Manifold +{ + Manifold output; + + auto in_smoothed_v = estimate_normals_and_smooth(pool, vertices_copy, normals_copy, opts.dist); + const Tree kd_tree = build_kd_tree_of_indices(in_smoothed_v, std::views::iota(0UL, in_smoothed_v.size())); + + // Find components + timer.start("Split components"); + std::cout << "Split components\n"; + // Note: the cross connection filtering needs to be synced with the inner loop, else there are issues + auto neighbor_map = calculate_neighbors(pool, in_smoothed_v, kd_tree, opts.k); + Util::Parallel::foreach(pool, std::views::iota(0UL, vertices_copy.size()), [&](const NodeID idx) { + filter_out_cross_connection(neighbor_map[idx], normals_copy, idx, opts.theta, opts.dist == Distance::Euclidean); + }); + + auto [component_vertices, + component_smoothed_v, + component_normals] = + split_components(pool, + neighbor_map, + std::move(vertices_copy), + std::move(normals_copy), + std::move(in_smoothed_v), + opts); + timer.end("Split components"); + // There is no guarantee that there is more than one component, and components can + // be highly non-uniform in terms of how many primitives they have. That means we cannot + // rely on this loop for good parallelization opportunities. + timer.start("Algorithm"); + for (size_t component_id = 0; component_id < component_vertices.size(); component_id++) { + std::cout << "Reconstructing component " << std::to_string(component_id) << " ... (" << component_vertices[component_id].size() << " vertices)" << std::endl; + + std::vector vertices_of_this = std::move(component_vertices[component_id]); + std::vector normals_of_this = std::move(component_normals[component_id]); + std::vector smoothed_v_of_this = std::move(component_smoothed_v[component_id]); + GEL_ASSERT(vertices_of_this.size() == normals_of_this.size()); + GEL_ASSERT(vertices_of_this.size() == smoothed_v_of_this.size()); + + // While I would like to move this up, there are some nontrivial changes made to each component + // inside split_components eve if we only have one component to worry about. + const auto indices_of_this = std::ranges::iota_view(0UL, smoothed_v_of_this.size()); + Tree kd_tree_of_this = build_kd_tree_of_indices(smoothed_v_of_this, indices_of_this); + + auto neighbor_map_of_this = calculate_neighbors(pool, smoothed_v_of_this, kd_tree_of_this, opts.k); + //Util::Parallel::foreach(pool, std::views::iota(0UL, smoothed_v_of_this.size()), [&](NodeID idx) { + // filter_out_cross_connection(neighbor_map_of_this[idx], normals_of_this, idx, opts.theta, + // opts.dist == Distance::EUCLIDEAN); + //}); + + auto res = component_to_manifold( + pool, + opts, + vertices_of_this, + normals_of_this, + smoothed_v_of_this, + kd_tree_of_this, + neighbor_map_of_this); + + output.merge(res); + // if (component_id == 0) { + // std::swap(output, res); + // } else { + // output.merge(res); + // } + } + timer.end("Algorithm"); + + return output; +} + +auto point_cloud_to_mesh( + const std::vector& vertices_in, + const std::vector& normals_in, + const RsROpts& opts) -> Manifold +{ + Util::RSRTimer timer; + ThreadPool pool; + timer.start("Whole process"); + timer.start("Validation"); + if (!normals_in.empty()) { + GEL_ASSERT_EQ(vertices_in.size(), normals_in.size(), "Vertices and normals must be the same size"); + } + for (const auto& point: vertices_in) { + GEL_ASSERT_FALSE(std::isnan(point[0]) || std::isnan(point[1]) || std::isnan(point[2]), "Bad point input"); + } + for (const auto& normal: normals_in) { + GEL_ASSERT_FALSE(std::isnan(normal[0]) || std::isnan(normal[1]) || std::isnan(normal[2]), "Bad normal input"); + GEL_ASSERT_NEQ(normal, Vec3(0.0), "Bad normal input"); + } + auto vertices_copy = vertices_in; + auto normals_copy = normals_in; + timer.end("Validation"); + + + // Estimate normals & orientation & weighted smoothing + timer.start("Estimate and smooth normals"); + std::vector in_smoothed_v = estimate_normals_and_smooth(pool, vertices_copy, normals_copy, opts.dist); + timer.end("Estimate and smooth normals"); + + + const auto indices = std::ranges::iota_view(0UL, in_smoothed_v.size()); + const Tree kd_tree = build_kd_tree_of_indices(in_smoothed_v, indices); + if (normals_in.empty()) { + std::cout << "correct normal orientation\n"; + timer.start("Correct normal orientation"); + correct_normal_orientation(pool, kd_tree, in_smoothed_v, normals_copy, opts.k); + timer.end("Correct normal orientation"); + } + + Manifold result = point_cloud_to_mesh_impl( + std::move(vertices_copy), + std::move(normals_copy), + timer, + pool, + opts); + timer.end("Whole process"); + const std::string line(40, '='); + std::cout << line << "\n\n"; + timer.show(); + return result; +} + +template +auto indexed_select(const Collection& collection, + const IndexRange& indices) -> std::vector +{ + std::vector result; + result.reserve(collection.size()); + for (auto idx : indices) { + result.push_back(collection.at(idx)); + } + return result; +} + +std::vector validate_normals(ThreadPool& pool, const std::vector& vertices, const std::vector& normals, const RsROpts& reconstruction_options) +{ + auto check = [&]() { + if (!normals.empty() && vertices.size() != normals.size()) { + std::cerr << "Bad normal input: normals must either be empty or the same size as vertices" << "\n"; + return false; + } + for (const auto& point: vertices) { + GEL_ASSERT_FALSE(std::isnan(point[0]) || std::isnan(point[1]) || std::isnan(point[2]), "Bad point input: Found a NaN coordinate."); + } + for (const auto& normal: normals) { + if (std::isnan(normal[0]) || std::isnan(normal[1]) || std::isnan(normal[2])) { + std::cerr << "Bad normal input: normal is NaN" << "\n"; + return false; + } + if (normal == Vec3(0.0)) { + std::cerr << "Bad normal input: normal is 0 vector" << "\n"; + return false; + } + } + return true; + }; + if (check()) { + return normals; + } else { + std::cout << "generating normals..."; + auto normals_copy = std::vector(); + auto smoothed_points = estimate_normals_and_smooth(pool, vertices, normals_copy, reconstruction_options.dist); + return normals_copy; + } + +} + +auto point_cloud_collapse_reexpand( + const std::vector& vertices_in, + const std::vector& normals_in, + const CollapseOpts& collapse_options, + const RsROpts& rsr_opts, + const ReexpandOptions& reexpand_opts) -> Manifold +{ + if (collapse_options.max_iterations == 0) { + return point_cloud_to_mesh(vertices_in, normals_in, rsr_opts); + } + Util::RSRTimer timer; + ThreadPool pool; + + timer.start("Whole process"); + timer.start("Validation"); + if (!normals_in.empty()) { + GEL_ASSERT_EQ(vertices_in.size(), normals_in.size(), "Vertices and normals must be the same size"); + } + for (const auto& point: vertices_in) { + GEL_ASSERT_FALSE(std::isnan(point[0]) || std::isnan(point[1]) || std::isnan(point[2]), "Bad point input"); + } + for (const auto& normal: normals_in) { + GEL_ASSERT_FALSE(std::isnan(normal[0]) || std::isnan(normal[1]) || std::isnan(normal[2]), "Bad normal input"); + GEL_ASSERT_NEQ(normal, Vec3(0.0), "Bad normal input"); + } + auto vertices_copy = vertices_in; + auto normals_copy = normals_in; + timer.end("Validation"); + + + // Estimate normals & orientation & weighted smoothing + timer.start("Estimate and smooth normals"); + std::vector in_smoothed_v = estimate_normals_and_smooth(pool, vertices_copy, normals_copy, collapse_options.distance); + timer.end("Estimate and smooth normals"); + + + + if (normals_in.empty()) { + std::cout << "correct normal orientation\n"; + timer.start("Correct normal orientation"); + const auto indices = std::ranges::iota_view(0UL, in_smoothed_v.size()); + const Tree kd_tree = build_kd_tree_of_indices(in_smoothed_v, indices); + correct_normal_orientation(pool, kd_tree, in_smoothed_v, normals_copy, rsr_opts.k); + timer.end("Correct normal orientation"); + } + + timer.start("Collapse"); + auto [collapse, point_cloud] = collapse_points(vertices_copy, normals_copy, collapse_options); + timer.end("Collapse"); + + auto [points_collapsed, normals_collapsed, indices_collapsed] = std::move(point_cloud); + + Manifold manifold = point_cloud_to_mesh_impl( + std::move(points_collapsed), + std::move(normals_collapsed), + timer, + pool, + rsr_opts); + + timer.start("Reexpand"); + if (reexpand_opts.enabled) + reexpand_points(manifold, std::move(collapse), reexpand_opts); + timer.end("Reexpand"); + + timer.end("Whole process"); + const std::string line(40, '='); + std::cout << line << "\n\n"; + timer.show(); + return manifold; +} + +auto point_cloud_normal_estimate(const std::vector& vertices, + const std::vector& normals, + const bool is_euclidean) -> NormalEstimationResult +{ + auto normals_copy = normals; + const auto dist = is_euclidean ? Distance::Euclidean : Distance::Tangent; + ThreadPool pool; + const auto smoothed_v = estimate_normals_and_smooth(pool, vertices, normals_copy, dist); + return {vertices, normals_copy, smoothed_v}; +} +} // namespace GEL::HMesh::RsR diff --git a/src/GEL/HMesh/RsR2.h b/src/GEL/HMesh/RsR2.h new file mode 100644 index 00000000..e487daff --- /dev/null +++ b/src/GEL/HMesh/RsR2.h @@ -0,0 +1,395 @@ +#ifndef GEL_HMesh_RsR2_hpp +#define GEL_HMesh_RsR2_hpp +#pragma once + +#include + +#include + +#include +#include + +#include + +/// @brief Rotation System Reconstruction +namespace HMesh::RSR +{ +using Vec3 = CGLA::Vec3d; +using Point = Vec3; +using NodeID = AMGraph::NodeID; + +using uint = Geometry::uint; + +inline constexpr NodeID InvalidNodeID = AMGraph::InvalidNodeID; +inline constexpr NodeID InvalidEdgeID = AMGraph::InvalidEdgeID; + +namespace detail +{ + template + using OrderedSet = Util::BTreeSet; + + template + using UnorderedSet = Util::HashSet; + + template + using Map = std::unordered_map; + + template + using OrderedMap = Util::BTreeMap; +} + + +double cal_radians_3d(const Vec3& branch, const Vec3& normal); +double cal_radians_3d(const Vec3& branch, const Vec3& normal, const Vec3& ref_vec); + +/// TODO: documentation +struct RsROpts { + int32_t genus = -1; + int32_t k = 70; + double r = 20; + double theta = 60; + int32_t n = 50; + Distance dist = Distance::Euclidean; + + bool is_face_normal = true; + bool is_face_loop = true; +}; + +/// Graph definition. The RsR graph here is integrated with the rotation system based on AMGraph +struct Vertex { + using NormalRep = std::int64_t; + + NormalRep normal_rep = InvalidNormalRep; + Vec3 coords = Vec3(0., 0., 0.); + Vec3 normal = Vec3(0., 0., 0.); + + static constexpr NormalRep InvalidNormalRep = -1; + static constexpr NormalRep CollisionRep = -2; +}; + +struct Neighbor { + double angle = 0.0; + uint v = -1; + uint tree_id = 0; + + Neighbor(const Vertex& u, const Vertex& v, const uint id) + { + this->v = id; + this->angle = cal_radians_3d(v.coords - u.coords, u.normal); + } + + // friend size_t hash_value(const Neighbor& p) + // { + // return std::hash()(p.v); + // } + bool operator==(const Neighbor& rhs) const + { + return v == rhs.v; + } + + bool operator<(const Neighbor& rhs) const + { + return angle < rhs.angle; + } + + // std::weak_ordering operator<=>(const Neighbor& rhs) const + // { + // if (this->v == rhs.v) return std::weak_ordering::equivalent; + // if (this->angle < rhs.angle) return std::weak_ordering::less; + // else return std::weak_ordering::greater; + // } +}; +static_assert(std::is_trivially_destructible_v); + +struct Edge { + NodeID source = InvalidNodeID; + NodeID target = InvalidNodeID; + int ref_time = 0; +}; + +class SimpGraph /*: public Util::SparseGraph*/ { + AMGraph graph; + std::vector m_edges; // Edge weight + +public: + using NodeID = decltype(graph)::NodeID; + static constexpr auto InvalidEdgeID = decltype(graph)::InvalidEdgeID; + + void reserve(size_t vertices, int k) + { + m_edges.reserve(vertices * k); + } + + // connected components + [[nodiscard]] + auto inner() const -> const decltype(graph)& + { + return graph; + } + + // generic algorithms + auto add_node() -> NodeID + { + return graph.add_node(); + } + + auto connect_nodes(const NodeID source, const NodeID target, const double weight = 0.) + { + const auto id = + graph.connect_nodes(source, target); + if (id == m_edges.size()) + m_edges.push_back(weight); + else + m_edges[id] = weight; + return id; + } + + [[nodiscard]] double get_weight(const NodeID n1, const NodeID n2) const + { + return m_edges[graph.find_edge(n1, n2)]; + } + + /// Disconnect nodes. This operation removes the edge from the edge maps of the two formerly connected + /// vertices, but the number of edges reported by the super class AMGraph is not decremented, so the edge is only + /// invalidated. Call cleanup to finalize removal. + void disconnect_nodes(const NodeID n0, const NodeID n1) + { + //return graph.remove_edge(n0, n1); + //remove_edge(n0, n1); + // if (graph.valid_node_id(n0) && graph.valid_node_id(n1)) { + // graph.edge_map[n0].erase(n1); + // graph.edge_map[n1].erase(n0); + // } + graph.erase_edge(n0, n1); + } + + [[nodiscard]] + auto find_edge(NodeID from, NodeID to) const + { + return graph.find_edge(from, to); + } + + [[nodiscard]] + auto node_ids() const + { + return graph.node_ids(); + } + + [[nodiscard]] + auto no_nodes() const -> size_t + { + return graph.no_nodes(); + } + + [[nodiscard]] + auto no_edges() const -> size_t + { + return graph.no_edges(); + } + + [[nodiscard]] + auto neighbors_lazy(NodeID from) const + { + return graph.neighbors_lazy(from); + } + + auto collapse(NodeID to_keep, NodeID to_remove) + { + for (auto n: neighbors_lazy(to_remove)) { + double weight = get_weight(n, to_remove); + connect_nodes(to_keep, n, weight); + } + graph.erase_node(to_remove); + + } +}; + +class RSGraph : private AMGraph { +public: + static constexpr auto InvalidEdgeID = std::nullopt; + + Geometry::ETF etf; + std::vector m_vertices; + std::vector> m_neighbors; + std::vector m_edges; + + void reserve(size_t nodes, int k) + { + m_vertices.reserve(nodes * k); + } + + [[nodiscard]] + auto no_nodes() const -> size_t + { + return AMGraph::no_nodes(); + } + + [[nodiscard]] + auto neighbors_lazy(NodeID n) const + { + return AMGraph::neighbors_lazy(n); + } + + [[nodiscard]] + auto shared_neighbors_lazy(NodeID n1, NodeID n2) const + { + return AMGraph::shared_neighbors_lazy(n1, n2); + } + + [[nodiscard]] + auto valence(NodeID n) const + { + return AMGraph::valence(n); + } + + EdgeID add_edge(const NodeID source, const NodeID target) + { + const EdgeID id = this->connect_nodes(source, target); + GEL_ASSERT_NEQ(id, AMGraph::InvalidEdgeID); + if (m_edges.size() == id) + m_edges.emplace_back(source, target); + else + m_edges[id] = Edge {.source = source, .target = target}; + + // insert neighbors + m_neighbors[source].emplace(Neighbor(m_vertices[source], m_vertices[target], target)); + m_neighbors[target].emplace(Neighbor(m_vertices[target], m_vertices[source], source)); + + //return { source, target }; + return id; + } + + NodeID add_node(const Vec3& p, const Vec3& in_normal = Vec3(0., 0., 0.)) + { + const NodeID n = AMGraph::add_node(); + GEL_ASSERT_EQ(m_vertices.size(), n); + m_vertices.emplace_back(Vertex::InvalidNormalRep, p, in_normal); + m_neighbors.emplace_back(); + //m_vertices[n] = Vertex{.id = n, .normal_rep = Vertex::InvalidNormalRep, .coords = p, .normal = in_normal }; + return n; + } + + void increment_ref_time(NodeID n1, NodeID n2) + { + auto edge = AMGraph::find_edge(n1, n2); + if (edge != AMGraph::InvalidEdgeID) { + m_edges[edge].ref_time += 1; + } + } + + [[nodiscard]] + std::optional find_edge(NodeID n1, NodeID n2) const + { + const EdgeID id = AMGraph::find_edge(n1, n2); + if (id == AMGraph::InvalidEdgeID) { + return std::nullopt; + } + return m_edges[id]; + } + + /// @brief Get last neighbor information + /// @param root: root vertex index + /// @param branch: current outgoing branch + /// @return reference to last neighbor struct + // TODO: const correctness + [[nodiscard]] + Neighbor& predecessor(const NodeID& root, const NodeID& branch) const + { + GEL_ASSERT(m_vertices.size() > root); + GEL_ASSERT(m_vertices.size() > branch); + auto& u = m_vertices.at(root); + auto& v = m_vertices.at(branch); + GEL_ASSERT(!m_neighbors[root].empty()); // we need at least one neighbor to return + Neighbor temp = {u, v, static_cast(branch)}; + auto iter = m_neighbors[root].lower_bound(temp); + if (iter == m_neighbors[root].begin()) iter = m_neighbors[root].end(); // Wrap around + //auto& f = iter->first; + //auto& s = iter->second; + //return {f, s}; + //return std::tie(f, s); + return const_cast(*(std::prev(iter))); + } + + /// @brief Get the next neighbor information + /// + /// @param root: root vertex index + /// @param branch: current outgoing branch + /// + /// @return reference to the next neighbor struct + [[nodiscard]] + const Neighbor& successor(const NodeID& root, const NodeID& branch) const + { + auto& u = m_vertices.at(root); + auto& v = m_vertices.at(branch); + GEL_ASSERT(!m_neighbors[root].empty()); // we need at least one neighbor to return + auto iter = m_neighbors[root].upper_bound(Neighbor(u, v, branch)); + if (iter == m_neighbors[root].end()) iter = m_neighbors[root].begin(); // Wrap around + //auto& f = iter->first; + //auto& s = iter->second; + //return {f, s}; + //return std::tie(f, s); + //return const_cast(*(iter)); + return (*iter); + } + +private: + /// @brief Get the neighbor information + /// @param root: root vertex index + /// @param branch: the outgoing branch + /// @return reference to the neighbor struct + Neighbor& get_neighbor_info(const NodeID root, const NodeID branch) + { + auto& u = this->m_vertices.at(root); + auto& v = this->m_vertices.at(branch); + auto iter = m_neighbors[root].lower_bound({u, v, static_cast(branch)}); + // TODO: tree_id does not invalidate ordered_neighbors, but it still has thread safety issues + //auto& f = iter->first; + //auto& s = iter->second; + //return {f, s}; + return const_cast(*(iter)); + } +public: + + void maintain_face_loop(const NodeID source, const NodeID target) + { + auto& this_v_tree = this->predecessor(source, target).tree_id; + auto& neighbor_tree = this->predecessor(target, source).tree_id; + + auto [fst, snd] = this->etf.insert(this_v_tree, neighbor_tree); + + get_neighbor_info(source, target).tree_id = fst; + get_neighbor_info(target, source).tree_id = snd; + } +}; + +// Algorithm + +/// Convert point cloud to a Manifold +/// @param vertices_in vertices of the point cloud +/// @param normals_in normals of the point cloud or empty vector +/// @param opts collapse options +/// @return reconstructed manifold mesh +auto point_cloud_to_mesh(const std::vector& vertices_in, + const std::vector& normals_in, + const RsROpts& opts) -> ::HMesh::Manifold; + +struct NormalEstimationResult { + std::vector vertices; + std::vector normals; + std::vector smoothed_v; +}; + +auto point_cloud_normal_estimate(const std::vector& vertices, + const std::vector& normals, + bool is_euclidean) -> NormalEstimationResult; + + +auto point_cloud_collapse_reexpand( + const std::vector& vertices, + const std::vector& normals, + const CollapseOpts& collapse_options, + const RsROpts& reconstruction_options, + const ReexpandOptions& reexpand_options) -> HMesh::Manifold; +} // namespace HMesh::RSR + +#endif // GEL_HMesh_RsR2_hpp diff --git a/src/GEL/HMesh/Walker.h b/src/GEL/HMesh/Walker.h index 79dc44b4..57f1805a 100644 --- a/src/GEL/HMesh/Walker.h +++ b/src/GEL/HMesh/Walker.h @@ -3,128 +3,148 @@ * Copyright (C) the authors and DTU Informatics * For license and list of authors, see ../../doc/intro.pdf * ----------------------------------------------------------------------- */ +#pragma once +#ifndef GEL_HMESH_WALKER_H +#define GEL_HMESH_WALKER_H /** @file Walker.h Contains class for walking a mesh. */ -#pragma once -#include +#include namespace HMesh { - /** Class for traversing the entities of a HMesh. This class can work as - both a vertex and a face circulator but also move in other ways. It is, - in fact, the only way to traverse the mesh from the users point of view. */ - class Walker - { - public: - /// construct from kernel and a halfedge - Walker(const ConnectivityKernel& _ck, HalfEdgeID _current); - - /// returned walker has made one step to the next halfedge - Walker next() const; - /// returned walker has made one step to the previous halfedge - Walker prev() const; - /// returned walker has made one step to the opposite halfedge - Walker opp() const; - - /// returned walker has circulated vertex clockwise one step - Walker circulate_vertex_cw() const; - /// returned walker has circulated vertex counterclockwise one step - Walker circulate_vertex_ccw() const; - - /// returned walker has circulated face clockwise one step - Walker circulate_face_cw() const; - /// returned walker has circulated face counterclockwise one step - Walker circulate_face_ccw() const; - - /// test if the walker has reached its initial halfedge - bool full_circle() const; - - /// number of steps taken - int no_steps() const; - - /// get ID of vertex pointed to by current halfedge of walker - VertexID vertex() const; - /// get ID of face owning current halfedge of walker - FaceID face() const; - /// get ID of current halfedge of walker - HalfEdgeID halfedge() const; - /// Get ID of either halfedge or ID - whichever has the smaller index. - HalfEdgeID hmin() const; - - /// assignment operator - Walker operator =(const Walker& w); - - private: - const ConnectivityKernel* ck; - HalfEdgeID current; - HalfEdgeID last; - int counter; - - Walker(const ConnectivityKernel& _ck, HalfEdgeID _current, HalfEdgeID _last, int _counter); - }; - - inline Walker::Walker(const ConnectivityKernel& _ck, HalfEdgeID _current) - : ck(&_ck), current(_current), last(_current), counter(0){} - - inline Walker::Walker(const ConnectivityKernel& _ck, HalfEdgeID _current, HalfEdgeID _last, int _counter) - : ck(&_ck), current(_current), last(_last), counter(_counter){} - - inline Walker Walker::next() const - { return Walker(*ck, ck->next(current), last, counter + 1); } - - inline Walker Walker::prev() const - { return Walker(*ck, ck->prev(current), last, counter + 1); } - - inline Walker Walker::opp() const - { return Walker(*ck, ck->opp(current), last, counter + 1); } - - inline Walker Walker::circulate_vertex_cw() const - { return Walker(*ck, ck->next(ck->opp(current)), last, counter + 1); } - - inline Walker Walker::circulate_vertex_ccw() const - { return Walker(*ck, ck->opp(ck->prev(current)), last, counter + 1); } - - inline Walker Walker::circulate_face_cw() const - { return Walker(*ck, ck->prev(current), last, counter + 1); } - - inline Walker Walker::circulate_face_ccw() const - { return Walker(*ck, ck->next(current), last, counter + 1); } - - inline bool Walker::full_circle() const - { return (counter > 0 && current == last) ? true : false; } - - inline int Walker::no_steps() const - { return counter; } - - inline VertexID Walker::vertex() const - { return ck->vert(current); } - - inline FaceID Walker::face() const - { return ck->face(current); } - - inline HalfEdgeID Walker::halfedge() const - { return current; } - - - inline HalfEdgeID Walker::hmin() const - { return (currentopp(current))?current:ck->opp(current); } - - - inline Walker Walker::operator =(const Walker& w) - { - current = w.current; - counter = w.counter; - ck = w.ck; - last = w.last; - return *this; - } +/// Class for traversing the entities of a HMesh. This class can work as +/// both a vertex and a face circulator but also move in other ways. It is, +/// in fact, the only way to traverse the mesh from the user's point of view. +class Walker { +public: + /// construct from kernel and a halfedge + Walker(const ConnectivityKernel& _ck, HalfEdgeID _current); + // Compiler requires a copy constructor when Walker is passed by value + Walker(const Walker& other) = default; + + /// @return new walker that made one step to the next halfedge + [[nodiscard]] Walker next() const; + /// @return new walker that made one step to the previous halfedge + [[nodiscard]] Walker prev() const; + /// @return new walker that made one step to the opposite halfedge + [[nodiscard]] Walker opp() const; + + /// @return new walker that circulated vertex clockwise one step + [[nodiscard]] Walker circulate_vertex_cw() const; + /// @return new walker that circulated vertex counterclockwise one step + [[nodiscard]] Walker circulate_vertex_ccw() const; + + /// @return new walker that circulated face clockwise one step + [[nodiscard]] Walker circulate_face_cw() const; + /// @return new walker that circulated face counterclockwise one step + [[nodiscard]] Walker circulate_face_ccw() const; + + /// test if the walker has reached its initial halfedge + [[nodiscard]] bool full_circle() const; + + /// number of steps taken + [[nodiscard]] int no_steps() const; + + /// get ID of vertex pointed to by the current halfedge of the walker + [[nodiscard]] VertexID vertex() const; + /// get ID of face owning current halfedge of walker + [[nodiscard]] FaceID face() const; + /// get ID of current halfedge of walker + [[nodiscard]] HalfEdgeID halfedge() const; + /// Get ID of either halfedge or ID - whichever has the smaller index. + [[nodiscard]] HalfEdgeID hmin() const; + + Walker& operator =(const Walker& w); + +private: + std::reference_wrapper ck; + HalfEdgeID current; + HalfEdgeID last; + int counter; + + Walker(const ConnectivityKernel& _ck, HalfEdgeID _current, HalfEdgeID _last, int _counter); +}; + +inline Walker::Walker(const ConnectivityKernel& _ck, const HalfEdgeID _current) + : ck(_ck), current(_current), last(_current), counter(0) {} + +inline Walker::Walker(const ConnectivityKernel& _ck, const HalfEdgeID _current, const HalfEdgeID _last, + const int _counter) + : ck(_ck), current(_current), last(_last), counter(_counter) {} + +inline Walker Walker::next() const +{ + return {ck, ck.get().next(current), last, counter + 1}; +} + +inline Walker Walker::prev() const +{ + return {ck, ck.get().prev(current), last, counter + 1}; +} + +inline Walker Walker::opp() const +{ + return {ck, ck.get().opp(current), last, counter + 1}; +} + +inline Walker Walker::circulate_vertex_cw() const +{ + return {ck, ck.get().next(ck.get().opp(current)), last, counter + 1}; +} + +inline Walker Walker::circulate_vertex_ccw() const +{ + return {ck, ck.get().opp(ck.get().prev(current)), last, counter + 1}; +} + +inline Walker Walker::circulate_face_cw() const +{ + return {ck, ck.get().prev(current), last, counter + 1}; +} + +inline Walker Walker::circulate_face_ccw() const +{ + return {ck, ck.get().next(current), last, counter + 1}; +} + +inline bool Walker::full_circle() const +{ + return (counter > 0 && current == last); +} + +inline int Walker::no_steps() const +{ + return counter; +} + +inline VertexID Walker::vertex() const +{ + return ck.get().vert(current); +} + +inline FaceID Walker::face() const +{ + return ck.get().face(current); +} + +inline HalfEdgeID Walker::halfedge() const +{ + return current; +} + +inline HalfEdgeID Walker::hmin() const +{ + return (current < ck.get().opp(current)) ? current : ck.get().opp(current); } +inline Walker& Walker::operator=(const Walker& w) = default; +} +#endif diff --git a/src/GEL/HMesh/curvature.h b/src/GEL/HMesh/curvature.h index 43836e87..e3f14779 100644 --- a/src/GEL/HMesh/curvature.h +++ b/src/GEL/HMesh/curvature.h @@ -9,21 +9,12 @@ * @brief Compute various curvature measures from meshes. */ -#ifndef __MESHEDIT_CURVATURE_H__ -#define __MESHEDIT_CURVATURE_H__ +#ifndef MESHEDIT_CURVATURE_H +#define MESHEDIT_CURVATURE_H #include #include -namespace CGLA -{ - class Vec3d; - class Vec2d; - class Mat2x2d; - class Mat3x3d; - class Ma4x4d; -} - namespace HMesh { class Manifold; diff --git a/src/GEL/HMesh/quad_valencify.cpp b/src/GEL/HMesh/quad_valencify.cpp index c781f823..d425ac5c 100644 --- a/src/GEL/HMesh/quad_valencify.cpp +++ b/src/GEL/HMesh/quad_valencify.cpp @@ -7,14 +7,14 @@ // #include "quad_valencify.h" -#include + +#include #include #include #include #include #include -#include #include #include diff --git a/src/GEL/HMesh/skeleton_to_FEQ.cpp b/src/GEL/HMesh/skeleton_to_FEQ.cpp index e2d5641c..cdb231be 100644 --- a/src/GEL/HMesh/skeleton_to_FEQ.cpp +++ b/src/GEL/HMesh/skeleton_to_FEQ.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -8,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -39,6 +39,7 @@ struct BranchMeshInfo { HMesh::VertexID vertex; }; +// FIXME: Consider replacing these maps with Util::HashMap or Util::BTreeMap using BranchMeshMap = map,BranchMeshInfo>; using Face2VertexMap = map; diff --git a/src/GEL/Util/Assert.h b/src/GEL/Util/Assert.h index b3113d3a..7f52e6a7 100644 --- a/src/GEL/Util/Assert.h +++ b/src/GEL/Util/Assert.h @@ -1,7 +1,6 @@ // // Created by Cem Akarsubasi on 5/28/25. // -// Assertion macros #ifndef GEL_ASSERT_H #define GEL_ASSERT_H @@ -22,9 +21,7 @@ /// See @link GEL/Util/Assert.h Assert.h @endlink for the actual macros. namespace Util::Assert::detail { - /// @privatesection - /// @private template concept has_to_str_operator = requires(T t, std::stringstream& os) { { os << t }; }; @@ -141,10 +138,10 @@ namespace Util::Assert::detail { } } - template + template requires has_eq constexpr auto gel_assert_eq_impl(Left&& l, Right&& r, auto expr_l, auto expr_r, auto file, auto line, auto func, Args... args) -> void - requires has_eq + { if (l != r) [[unlikely]] { @@ -152,10 +149,9 @@ namespace Util::Assert::detail { } } - template + template requires has_eq constexpr auto gel_assert_neq_impl(Left&& l, Right&& r, auto expr_l, auto expr_r, auto file, auto line, auto func, Args... args) -> void - requires has_eq { if (l == r) [[unlikely]] { gel_assert_neq_failure(file, line, func, expr_l, expr_r, l, r, args...); diff --git a/src/GEL/Util/AssociativeContainers.h b/src/GEL/Util/AssociativeContainers.h new file mode 100644 index 00000000..d9953838 --- /dev/null +++ b/src/GEL/Util/AssociativeContainers.h @@ -0,0 +1,146 @@ +// +// Created by Cem Akarsubasi on 8/7/25. +// + +/// @file AssociativeContainers.h +/// @brief Type definitions for sets and maps + +#ifndef GEL_ASSOCIATIVECONTAINERS_H +#define GEL_ASSOCIATIVECONTAINERS_H + +#include +#include + +#include + +namespace Util +{ +template , typename Eq = phmap::EqualTo> +using HashMap = phmap::flat_hash_map; + +template > + requires std::is_nothrow_copy_constructible_v +using BTreeMap = phmap::btree_map; + +template > + requires std::is_nothrow_copy_constructible_v +using BTreeMultiMap = phmap::btree_multimap; + +template , typename Eq = phmap::EqualTo> +using HashSet = phmap::flat_hash_set; + +template > + requires std::is_nothrow_copy_constructible_v +using BTreeSet = phmap::btree_set; + +template > + requires std::is_nothrow_copy_constructible_v +using BTreeMultiSet = phmap::btree_multiset; + +using phmap::erase_if; + +// TODO: Might consider creating a header for common concepts used in GEL +namespace Concepts +{ + /// Checks if a range of type Range yields elements of type T + template + concept RangeYields = std::ranges::range && std::is_same_v>, T>; +} + + +/// A priority queue sorted by value. Sorted in ascending order by default. +/// +/// @details This class generalizes a priority queue by allowing key ordering relations to be stored indirectly +/// This means that keys can be removed arbitrarily and not just from the front. Existing keys can be updated by +/// reinserting them to the queue. +/// +/// Note that the memory footprint of this is substantially larger than an ordinary vector-based min/max heap. In +/// addition, the internal queue is always kept sorted, meaning there is a potentially higher up-front cost to +/// constructing this. So make sure you really need the mutability or really benefit from it when using this over +/// a generational priority queue. +template , + typename Eq = phmap::EqualTo, + typename Compare = phmap::Less> +class MutablePriorityQueue { + Util::HashMap m_distance_map; + + struct CompareFunctor { + const MutablePriorityQueue& self; + + bool operator()(const Key& key1, const Key& key2) const + { + Compare cmp; + return cmp(self.m_distance_map.at(key1), self.m_distance_map.at(key2)); + } + }; + + Util::BTreeSet m_queue; + +public: + using value_type = std::pair; + using size_type = decltype(m_queue)::size_type; + + MutablePriorityQueue() : m_queue(CompareFunctor{*this}) {}; + + /// Remove the first value in the queue + value_type pop_front() + { + GEL_ASSERT(!m_queue.empty()); + const auto key = *m_queue.begin(); + m_queue.erase(key); + const auto value = m_distance_map.at(key); + m_distance_map.erase(key); + return std::make_pair(key, value); + } + + [[nodiscard]] + size_type size() const + { + return m_queue.size(); + } + + [[nodiscard]] + bool empty() const + { + return m_queue.empty(); + } + + /// Inserts or updates the given keys + /// @details For n keys inside the queue and m keys in the given range, runs in O(m log m+n) time + template requires Concepts::RangeYields + void insert_range(KVs&& kvs) + { + // remove keys + for (const auto& [key, _] : kvs) { + if (m_distance_map.contains(key)) + m_queue.erase(key); + } + // write new distances + for (const auto& [key, value] : kvs) { + m_distance_map[key] = value; + } + // reinsert every key + for (const auto& [key, _] : kvs) { + m_queue.insert(key); + } + } + + /// Removes the given keys + /// @details For n keys inside the queue and m keys in the given range, runs in O(m log n) time + template requires Concepts::RangeYields + void remove_range(KeyRange&& keys) + { + for (const auto& key : keys) { + if (m_distance_map.contains(key)) + m_queue.erase(key); + } + for (const auto& key : keys) { + m_distance_map.erase(key); + } + } +}; +} + +#endif //GEL_ASSOCIATIVECONTAINERS_H diff --git a/src/GEL/Util/InplaceVector.h b/src/GEL/Util/InplaceVector.h new file mode 100644 index 00000000..9b4c1eaf --- /dev/null +++ b/src/GEL/Util/InplaceVector.h @@ -0,0 +1,281 @@ +// +// Created by Cem Akarsubasi on 5/13/25. +// + +#ifndef INPLACE_VECTOR_H +#define INPLACE_VECTOR_H +#include +#include + +#include + +namespace Util { + + /// Variable-sized container that stores its data vector inside it +template +class InplaceVector { +public: + using value_type = T; + using size_type = size_t; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = T*; + using const_iterator = const T*; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + +private: + size_type m_size = 0; + T m_data[N]; +public: + + // constructors and destructors + + constexpr InplaceVector() noexcept = default; + + explicit constexpr InplaceVector(const size_type size) : m_size(0) + { + GEL_ASSERT(size <= N); + for (size_type i = 0; i < size; ++i) { + new (&m_data[i]) T(); + // emplace_back(); + } + } + + explicit constexpr InplaceVector(const size_type size, const T& value) : m_size(0) + { + GEL_ASSERT(size <= N); + for (size_type i = 0; i < size; ++i) { + new (&m_data[i]) T(value); + // emplace_back(value); + } + } + + template + constexpr InplaceVector(InputIt first, InputIt last) + { + for (; first != last; ++first) { + push_back(*first); + } + } + + constexpr InplaceVector(const InplaceVector& other) { + for (size_t i = 0; i < other.m_size; ++i) { + m_data[i] = other.m_data[i]; + } + m_size = other.m_size; + + } + + constexpr InplaceVector(InplaceVector&& other) noexcept + { + m_size = other.m_size; + for (size_t i = 0; i < other.m_size; ++i) { + m_data[i] = std::move(other.m_data[i]); + } + other.m_size = 0; + } + + constexpr ~InplaceVector() + { + if constexpr (!std::is_trivially_destructible_v) { + for (size_type i = 0; i < m_size; ++i) { + m_data[i].~T(); + } + } + } + + constexpr InplaceVector& operator=(const InplaceVector& other) + { + if (this != &other) { + for (size_type i = 0; i < other.m_size; ++i) { + m_data[i] = other.m_data[i]; + } + m_size = other.m_size; + } + return *this; + } + + constexpr InplaceVector& operator=(InplaceVector&& other) noexcept + { + if (this != &other) { + for (size_type i = 0; i < other.m_size; ++i) { + m_data[i] = std::move(other.m_data[i]); + } + m_size = other.m_size; + other.m_size = 0; + } + return *this; + } + + // Element access + + constexpr reference operator[](size_t index) { + return *(m_data+index); + } + + constexpr const_reference operator[](size_t index) const { + return *(m_data+index); + } + + constexpr reference at(size_t index) { + GEL_ASSERT(index < m_size); + return *(m_data+index); + } + + [[nodiscard]] constexpr const_reference at(size_t index) const { + GEL_ASSERT(index < m_size); + return *(m_data+index); + } + + constexpr reference front() + { + return *m_data; + } + + [[nodiscard]] constexpr const_reference front() const + { + return *m_data; + } + + constexpr reference back() + { + return *(m_data+m_size-1); + } + + [[nodiscard]] constexpr const_reference back() const + { + return *(m_data+m_size-1); + } + + constexpr pointer data() noexcept + { + return m_data; + } + + [[nodiscard]] constexpr const_pointer data() const noexcept + { + return m_data; + } + + // Iterators + + constexpr iterator begin() + { + return m_data; + } + + constexpr iterator end() + { + return m_data + m_size; + } + + [[nodiscard]] + constexpr const_iterator begin() const + { + return m_data; + } + + [[nodiscard]] + constexpr const_iterator end() const + { + return m_data + m_size; + } + + [[nodiscard]] constexpr const_iterator cbegin() const + { + return m_data; + } + + [[nodiscard]] constexpr const_iterator cend() const + { + return m_data + m_size; + } + + constexpr reverse_iterator rbegin() + { + return std::reverse_iterator(end()); + } + + constexpr reverse_iterator rend() + { + return std::reverse_iterator(begin()); + } + + [[nodiscard]] constexpr const_reverse_iterator crbegin() const + { + return std::reverse_iterator(cend()); + } + + [[nodiscard]] constexpr const_reverse_iterator crend() const + { + return std::reverse_iterator(cbegin()); + } + + // Capacity + + [[nodiscard]] constexpr bool empty() const + { + return (m_size == 0); + } + + [[nodiscard]] constexpr size_type size() const + { + return m_size; + } + + [[nodiscard]] constexpr size_type max_size() const + { + return N; + } + + constexpr void reserve(const size_type new_cap) + { + // we literally can't do anything + GEL_ASSERT(new_cap <= N); + } + + [[nodiscard]] constexpr size_type capacity() const + { + return N; + } + + // Modifiers + + constexpr void clear() + { + for (size_type i = 0; i < m_size; ++i) { + m_data[i].~T(); + } + m_size = 0; + } + + constexpr void push_back(const T& value) + { + GEL_ASSERT(m_size < N); + new (&m_data[m_size++]) T(value); + // m_data[m_size++] = value; + } + + + constexpr T pop_back() + { + GEL_ASSERT(m_size > 0); + return std::move(m_data[--m_size]); + } + + template < class... Args > + constexpr void emplace_back(Args&&... args) + { + GEL_ASSERT(m_size < N); + new (m_data + m_size) T(std::forward(args)...); + m_size++; + } +}; + static_assert(std::ranges::viewable_range&>); + +} + +#endif //INPLACE_VECTOR_H diff --git a/src/GEL/Util/ParallelAdapters.h b/src/GEL/Util/ParallelAdapters.h new file mode 100644 index 00000000..3f63b57e --- /dev/null +++ b/src/GEL/Util/ParallelAdapters.h @@ -0,0 +1,815 @@ +// +// Created by Cem Akarsubasi on 5/5/25. +// +// TODO: examples + +#ifndef GEL_UTIL_PARALLELADAPTERS_H +#define GEL_UTIL_PARALLELADAPTERS_H +#pragma once + +#include +#include +#include +#ifdef _MSC_VER +#include // std::size +#include // std::numeric_limits +#endif + +namespace Util { + /// Concepts for constraining the inputs of parallel adapters + namespace Concepts { + + /// A range with random access that knows its size in constant time + template concept SizedRange = std::ranges::sized_range && std::ranges::random_access_range; + + /// A range with random access that knows its size in constant time that can be resized + template concept ResizableRange = SizedRange && requires(R& r, size_t size) { + { r.reserve(size) }; + { r.resize(size) }; + }; + + /// Returns the value inside a range + template using Item = std::ranges::range_value_t; + + /// Maintains the value classification of T + template using Forward = decltype(std::forward(std::declval())); + + template concept UnaryFunction = requires(A a, F f) { + { std::is_invocable() }; + { f(a) } -> std::convertible_to; + }; + + template concept BinaryFunction = requires(A a, B b, F f) { + { std::is_invocable() }; + { f(a, b) } -> std::convertible_to; + }; + + template concept TernaryFunction + = requires(A a, B b, C c, F f) { + { std::is_invocable() }; + { f(a, b, c) } -> std::convertible_to; + }; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange + /// @endcode + template concept ForeachFunctor = SizedRange && UnaryFunction, void>; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept MapFunctor + = SizedRange && SizedRange && UnaryFunction, Item>; + + /// @code + /// Functor: size_t -> 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept EnumMapper + = SizedRange && SizedRange && BinaryFunction, Item>; + + /// @code + /// Functor: 'a -> 'b -> 'c where I1: SizedRange, I2: SizedRange + /// @endcode + template concept Foreach2Functor + = SizedRange && SizedRange && BinaryFunction, Item, void>; + + /// @code + /// Functor: size_t -> 'a -> 'b -> 'c where I1: SizedRange, I2: SizedRange + /// @endcode + template concept EnumForeach2Functor + = SizedRange && SizedRange && TernaryFunction, Item, void>; + + /// @code + /// Functor: 'a -> 'b -> 'c where I1: SizedRange, I2: SizedRange, O: SizedRange + /// @endcode + template concept Map2Functor + = SizedRange && SizedRange && SizedRange && BinaryFunction, Item, Item>; + + /// @code + /// Functor: 'a -> 'b -> 'c where I1: SizedRange, I2: SizedRange, O: SizedRange + /// @endcode + template concept EnumMap2Functor + = SizedRange && SizedRange && SizedRange + && TernaryFunction, Item, Item>; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept FilterFunctor + = SizedRange && ResizableRange && UnaryFunction, bool> + && std::same_as, Item>; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept MapFilterFunctor + = SizedRange && ResizableRange && UnaryFunction, std::optional>>; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept EnumMapFilterFunctor + = SizedRange && ResizableRange && BinaryFunction, std::optional>>; + + /// @code + /// Functor: 'a -> 'b where I: SizedRange, O: SizedRange + /// @endcode + template concept EnumMap2FilterFunctor + = SizedRange && SizedRange && ResizableRange + && TernaryFunction, Item, std::optional>>; + + /// @code + /// Functor: 'a -> 'b -> Option Pair 'c * 'd + /// where I1: SizedRange, + /// I2: SizedRange, + /// O1: ResizableRange + /// O2: ResizableRange + /// @endcode + template concept Map2Filter2Functor + = SizedRange && SizedRange && SizedRange && SizedRange + && BinaryFunction, Item, std::optional, Item>>>; + + /// @code + /// Functor: size_t -> 'a -> 'b -> Option Pair 'c * 'd + /// where I1: SizedRange, + /// I2: SizedRange, + /// O1: ResizableRange + /// O2: ResizableRange + /// @endcode + template concept EnumMap2Filter2Functor + = SizedRange && SizedRange && SizedRange && SizedRange + && TernaryFunction, Item, std::optional, Item>>>; + + template concept FoldFunctor + = SizedRange && BinaryFunction, State>; + } // namespace Concepts + + /// Helper functions for parallel algorithms + /// @internal + namespace ParallelUtil { + /// @brief variadic function to find the shortest iterator + /// @tparam Iterator The first iterator + /// @tparam Iterators Other iterators + /// @param list1 The first iterator + /// @param lists Further iterators + /// @return The length of the shortest iterator + template + constexpr size_t smallest_size(const Iterator& list1, const Iterators&... lists) + { + size_t min_size = std::numeric_limits::max(); + for (std::array sizes = {list1.size(), std::size(lists...)}; + auto size: sizes) { + if (size < min_size) { min_size = size; } + } + return min_size; + } + + /// @brief Ceiling division + /// @tparam IntegerType1 Integral type + /// @tparam IntegerType2 Integral type + /// @param lhs lhs + /// @param rhs rhs + /// @return ceil(lhs / div) + template + requires std::is_integral_v && std::is_integral_v + constexpr IntegerType1 div_ceil(IntegerType1 lhs, IntegerType2 rhs) + { + return lhs / rhs + (lhs % rhs != 0); + } + + /// @brief Wraps up outputs to a value or a reference pair depending on the value classification + /// @return either @code std::pair(T1&, T2&) or std::pair(T1, T2) @endcode + template auto make_pair_wrapper(T1&& t1, T2&& t2) + { + if constexpr (std::is_lvalue_reference_v && std::is_lvalue_reference_v) { + return std::make_pair(std::ref(t1), std::ref(t2)); + } + else { + return std::make_pair(std::forward(t1), std::forward(t2)); + } + } + + // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ... + static constexpr std::size_t CACHE_LINE_SIZE = 64; // 64 bytes is as good a guess as any + + struct alignas(CACHE_LINE_SIZE) CacheLineCounter { + size_t inner; + }; + + /// @brief Moves chunks and resizes the output array for + /// @param target output array + /// @param counters the counter vector containing chunk sizes + /// @param max_chunk_size gap size between the chunks + template + void fix_chunks(Target& target, const std::vector& counters, size_t max_chunk_size) + { + size_t total_size = 0; + for (const auto& counter: counters) { total_size += counter.inner; } + + auto end_of_chunk1 = target.begin() + counters.at(0).inner; + for (size_t chunk = 1; chunk < counters.size(); ++chunk) { + auto this_chunk_size = counters[chunk].inner; + auto chunk_begin = target.begin() + chunk * max_chunk_size; + auto chunk_end = chunk_begin + this_chunk_size; + std::copy(chunk_begin, chunk_end, end_of_chunk1); + end_of_chunk1 += this_chunk_size; + } + target.resize(total_size); + } + } // namespace ParallelUtil + + /// Parallel algorithms + namespace Parallel { + using namespace Concepts; + + /// @brief Runs the given closure for each element of the input iterator + /// @tparam F a unary function type of the form T -> void + /// @tparam InputIt An input iterator type that yields elements of T + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param f the function to map over + /// + template F> + auto foreach (IExecutor& pool, const InputIt& it, F && f) -> void + { + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return; } + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([i, reduced_size, work_size, &it, &f] { + const auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { f(it[j]); } + }); + } + pool.waitAll(); + } + + /// @brief Runs the given closure for each element of two input iterators + /// @tparam F a binary function type of the form (T, U) -> void + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam InputIt2 An input iterator type that yields elements of U + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param f the function to map over + template F> + auto foreach2(IExecutor& pool, InputIt1& it1, InputIt2& it2, F&& f) -> void + { + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return; } + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([i, reduced_size, work_size, &it1, &it2, &f] { + const auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { f(it1[j], it2[j]); } + }); + } + pool.waitAll(); + } + + /// @brief Runs the given closure for each element of two input iterators + /// @tparam F a binary function type of the form (T, U) -> void + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam InputIt2 An input iterator type that yields elements of U + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param f the function to map over + template F> + auto enumerate_foreach2(IExecutor& pool, InputIt1& it1, InputIt2& it2, F&& f) -> void + { + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return; } + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([i, reduced_size, work_size, &it1, &it2, &f] { + const auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { f(j, it1[j], it2[j]); } + }); + } + pool.waitAll(); + } + + /// @brief Map over an iterator. + /// @tparam F a unary function type of the form T -> U + /// @tparam InputIt An input iterator type that yields elements of T + /// @tparam OutputIt An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param out the output iterator + /// @param f the function to map over + /// @return the forwarded output iterator + /// + template F> + auto map(IExecutor& pool, const InputIt& it, OutputIt&& out, F&& f) -> Forward + { + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = work_size / pool_size + (work_size % pool_size != 0); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + out.resize(work_size); + } + + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([&out, i, reduced_size, work_size, &it, &f] { + auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { out[j] = f(it[j]); } + }); + } + pool.waitAll(); + return std::forward(out); + } + + /// @brief Map over an iterator with the indexes + /// @tparam F a binary function type of the form (size_t, T) -> U + /// @tparam InputIt An input iterator type that yields elements of T + /// @tparam OutputIt An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param out the output iterator + /// @param f the function to map over + /// @return the forwarded output iterator. + template F> + auto enumerate_map(IExecutor& pool, const InputIt& it, OutputIt&& out, F&& f) -> decltype(out) + { + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = work_size / pool_size + (work_size % pool_size != 0); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + out.resize(work_size); + } + + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([&out, i, reduced_size, work_size, &it, &f] { + auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { out[j] = f(j, it[j]); } + }); + } + pool.waitAll(); + return std::forward(out); + } + + /// @brief Maps over two input iterators at the same time. If the iterators are of different length, + /// iterates up to the end of the shorter iterator. + /// @tparam F a unary function type of the form (T, U) -> V + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam InputIt2 An input iterator type that yields elements of U + /// @tparam OutputIt An output iterator type that takes elements of type V + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param out the output iterator + /// @param f the function to map over + /// @return the output iterator by value + /// + template F> + auto map2(IExecutor& pool, const InputIt1& it1, const InputIt2& it2, OutputIt&& out, F&& f) -> decltype(out) + { + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + out.resize(work_size); + } + + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([&out, i, reduced_size, work_size, &it1, &it2, &f] { + auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { out[j] = f(it1[j], it2[j]); } + }); + } + pool.waitAll(); + return std::forward(out); + } + + /// @brief Map over an iterator with the indexes + /// @tparam F a ternary function type of the form (size_t, T, U) -> V + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam InputIt2 An input iterator type that yields elements of U + /// @tparam OutputIt An output iterator type that takes elements of type V + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param out the output iterator + /// @param f the function to map over + /// @return the forwarded output iterator. + /// + template F> + auto enumerate_map2(IExecutor& pool, const InputIt1& it1, const InputIt2& it2, OutputIt&& out, F&& f) + -> decltype(out) + { + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + out.resize(work_size); + } + + for (size_t i = 0; i < pool_size; ++i) { + pool.addTask([&out, i, reduced_size, work_size, &it1, &it2, &f] { + auto max_size = std::min((i + 1) * reduced_size, work_size); + for (auto j = i * reduced_size; j < max_size; ++j) { out[j] = f(j, it1[j], it2[j]); } + }); + } + pool.waitAll(); + return std::forward(out); + } + + /// @brief perform a filter + /// @tparam F a unary function predicate of the form T -> bool + /// @tparam InputIt An input iterator type that yields elements of T + /// @tparam OutputIt An output iterator type that takes elements of T + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param out the output iterator + /// @param p the predicate + /// @return the output iterator by value + /// + template F> + auto filter(IExecutor& pool, const InputIt& it, OutputIt&& out, F&& p) -> decltype(out) + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (size_t thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id]; + thread_counter.inner = 0; + pool.addTask([&out, thread_id, reduced_size, work_size, &it, &p, &thread_counter] { + auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& value = it[j]; + if (p(value)) { + out[begin + thread_counter.inner] = value; + thread_counter.inner++; + } + } + }); + } + pool.waitAll(); + + ParallelUtil::fix_chunks(out, counters, reduced_size); + + return std::forward(out); + } + + /// @brief perform a map and a filter operation simultaneously + /// @tparam F a unary function type of the form T -> std::optional U + /// @tparam InputIt An input iterator type that yields elements of T + /// @tparam OutputIt An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param out the output iterator + /// @param f the function to map-filter over + /// @return the output iterator by value + /// + template F> + auto map_filter(IExecutor& pool, const InputIt& it, OutputIt&& out, F&& f) -> decltype(out) + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (auto thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id].inner; + thread_counter = 0; + pool.addTask([&, thread_id, reduced_size, work_size] { + const auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + const auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& input_value = it[j]; + if (const auto&& result = f(input_value); result.has_value()) { + out[begin + thread_counter] = *result; + thread_counter++; + } + } + }); + } + pool.waitAll(); + + ParallelUtil::fix_chunks(out, counters, reduced_size); + + return std::forward(out); + } + + /// @brief perform a map and a filter operation simultaneously over an enumerated iterator + /// @tparam F a binary function type of the form (size_t, T) -> std::optional U + /// @tparam InputIt An input iterator type that yields elements of T + /// @tparam OutputIt An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it the input iterator + /// @param out the output iterator + /// @param f the function to map-filter over + /// @return the output iterator by value + /// + template F> + auto enumerate_map_filter(IExecutor& pool, const InputIt& it, OutputIt&& out, F&& f) -> decltype(out) + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (size_t thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id]; + thread_counter.inner = 0; + pool.addTask([&out, thread_id, reduced_size, work_size, &it, &f, &thread_counter] { + auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& input_value = it[j]; + if (const auto&& result = f(j, input_value); result.has_value()) { + auto inner = std::move(result.value()); + out[begin + thread_counter.inner] = inner; + thread_counter.inner++; + } + } + }); + } + pool.waitAll(); + + ParallelUtil::fix_chunks(out, counters, reduced_size); + + return std::forward(out); + } + + /// @brief perform a map and a filter operation simultaneously over two input iterators + /// @tparam F a binary function type of the form (T, U) -> std::optional V + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam InputIt2 An input iterator type that yields elements of U + /// @tparam OutputIt An output iterator type that takes elements of type V + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param out the output iterator + /// @param f the function to map-filter over + /// @return the output iterator by value + /// + template F> + auto map2_filter(IExecutor& pool, const InputIt1& it1, const InputIt2& it2, OutputIt&& out, F&& f) + -> decltype(out) + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::forward(out); } + if (out.size() != work_size) { + out.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (size_t thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id]; + thread_counter.inner = 0; + pool.addTask([&out, thread_id, reduced_size, work_size, &it1, &f, &thread_counter, &it2] { + auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& input_value1 = it1[j]; + const auto& input_value2 = it2[j]; + const auto&& result = f(input_value1, input_value2); + if (result.has_value()) { + auto&& inner = result.value(); + out[begin + thread_counter.inner] = inner; + thread_counter.inner++; + } + } + }); + } + pool.waitAll(); + + ParallelUtil::fix_chunks(out, counters, reduced_size); + + return std::forward(out); + } + + /// @brief perform a map operation over two input iterators and a filter operation through two output iterators + /// @tparam F a binary function type of the form (T, U) -> std::optional std::pair V, W + /// @tparam InputIt1 An input iterator type that yields elements of T + /// @tparam OutputIt1 An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param out1 the first output iterator + /// @param out2 the second output iterator + /// @param f the function to map-filter over + /// @return the pair of output iterators by value + /// + template F> + auto map2_filter2(IExecutor& pool, const InputIt1& it1, const InputIt2& it2, OutputIt1&& out1, OutputIt2&& out2, + F&& f) -> std::pair + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { return std::make_pair(std::forward(out1), std::forward(out2)); } + if (out1.size() != work_size) { + out1.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out1.resize(work_size); + } + if (out2.size() != work_size) { + out2.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out2.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (size_t thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id]; + thread_counter.inner = 0; + pool.addTask([&out1, thread_id, reduced_size, work_size, &it1, &f, &thread_counter, &it2, out2] { + auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& input_value1 = it1[j]; + const auto& input_value2 = it2[j]; + const auto result = f(input_value1, input_value2); + if (result.has_value()) { + auto [inner1, inner2] = result.value(); + out1[begin + thread_counter.inner] = std::move(inner1); + out2[begin + thread_counter.inner] = std::move(inner2); + thread_counter.inner++; + } + } + }); + } + pool.waitAll(); + + + ParallelUtil::fix_chunks(out1, counters, reduced_size); + ParallelUtil::fix_chunks(out2, counters, reduced_size); + + return std::make_pair(std::forward(out1), std::forward(out2)); + } + + /// @brief perform a map operation over two input iterators and a filter operation through two output iterators + /// @tparam F a ternary function type of the form (size_t, T, U) -> std::optional std::pair V, W + /// @tparam I1 An input iterator type that yields elements of T + /// @tparam O1 An output iterator type that takes elements of type U + /// @param pool The threadpool to use + /// @param it1 the first input iterator + /// @param it2 the second input iterator + /// @param out1 the first output iterator + /// @param out2 the second output iterator + /// @param f the function to map-filter over + /// @return the pair of output iterators by value + template F> + auto enumerate_map2_filter2(IExecutor& pool, I1&& it1, I2&& it2, O1&& out1, O2&& out2, F&& f) + -> std::pair, Forward> + { + // strategy: we allocate out to the same size as it, every thread has its own counter, and when they are all + // done, we perform a serial memcpy. memcpy is memory-bound, meaning there is not a lot to gain from + // parallelization. + const auto pool_size = pool.size(); + const auto work_size = ParallelUtil::smallest_size(it1, it2); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + if (work_size == 0) { + return ParallelUtil::make_pair_wrapper(std::forward(out1), std::forward(out2)); + } + if (out1.size() != work_size) { + out1.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out1.resize(work_size); + } + if (out2.size() != work_size) { + out2.reserve(work_size); + // safety post-condition: we need to manually resize out to the correct size at the end + out2.resize(work_size); + } + using ParallelUtil::CacheLineCounter; + std::vector counters(pool_size, CacheLineCounter(0)); + for (size_t thread_id = 0; thread_id < pool_size; ++thread_id) { + auto& thread_counter = counters[thread_id]; + thread_counter.inner = 0; + pool.addTask([&out1, thread_id, reduced_size, work_size, &it1, &f, &thread_counter, &it2, &out2] { + auto max_size = std::min((thread_id + 1) * reduced_size, work_size); + auto begin = thread_id * reduced_size; + for (auto j = begin; j < max_size; ++j) { + const auto& input_value1 = it1[j]; + const auto& input_value2 = it2[j]; + auto result = f(j, input_value1, input_value2); + if (result.has_value()) { + auto [inner1, inner2] = std::move(result.value()); + out1[begin + thread_counter.inner] = inner1; + out2[begin + thread_counter.inner] = inner2; + thread_counter.inner++; + } + } + }); + } + pool.waitAll(); + + ParallelUtil::fix_chunks(out1, counters, reduced_size); + ParallelUtil::fix_chunks(out2, counters, reduced_size); + + return ParallelUtil::make_pair_wrapper(std::forward(out1), std::forward(out2)); + } + + namespace detail { + /// Serial fold + template F> + auto fold(InputIt&& it, State&& initial, F&& f) -> State + { + State next = initial; + for (size_t i = 0; i < it.size(); ++i) { next = f(next, it[i]); } + return next; + } + } // namespace detail + + // 'state -> 'a collection -> ('state -> 'a -> 'state) -> 'state + /// @brief Perform a parallel left fold + /// @details + /// @code Performs f(..., f(I[2], f([I[1], f(I[0], initial)))...) @endcode + /// @tparam F a binary function of type State -> Item I -> State + /// @tparam State the state type + /// @tparam I Input range + /// @param pool The threadpool to use + /// @param it The input iterator + /// @param initial Initial value of the state + /// @param identity The left identity with respect to f. For example the left identity with respect to addition + /// is the 0 matrix and the left identity with respect to multiplication is the identity matrix. + /// @param f hello + /// @return Folded state + template F> + auto fold(IExecutor& pool, I&& it, State&& initial, State&& identity, F&& f) -> State + { + const auto pool_size = pool.size(); + const auto work_size = it.size(); + const auto reduced_size = ParallelUtil::div_ceil(work_size, pool_size); + + std::vector final(pool_size, identity); + + for (size_t tid = 0; tid < pool_size; ++tid) { + auto max_size = std::min((tid + 1) * reduced_size, work_size); + auto begin = tid * reduced_size; + + pool.addTask([&] { + State&& next = identity; + for (size_t i = begin; i < max_size; ++i) { next = f(next, it[i]); } + final[tid] = next; + }); + } + pool.waitAll(); + return fold(final, initial, std::forward(f)); + } + } // namespace Parallel +} // namespace Util + +#endif \ No newline at end of file diff --git a/src/GEL/Util/RSRTimer.h b/src/GEL/Util/RSRTimer.h new file mode 100644 index 00000000..46684b48 --- /dev/null +++ b/src/GEL/Util/RSRTimer.h @@ -0,0 +1,86 @@ +// +// Created on 4/24/25. +// +#pragma once +#ifndef GEL_HMESH_RSRTIMER_H +#define GEL_HMESH_RSRTIMER_H + +#include +#include +#include +#include + +namespace Util { + /// Timer class + /// FIXME: The other ancient timer should probably be removed from the library and replaced with this + struct RSRTimer { + RSRTimer() = default; + + //RSRTimer(const RSRTimer&) = delete; + //RSRTimer& operator=(const RSRTimer&) = delete; + + private: + using time_point = std::chrono::high_resolution_clock::time_point; + + struct Time { + time_point start; + time_point end; + }; + + std::unordered_map m_times; + + public: + RSRTimer& create(const std::string& name) { + m_times.insert_or_assign( + name, + Time{std::chrono::high_resolution_clock::now(), std::chrono::high_resolution_clock::now()} + ); + + return *this; + } + + RSRTimer& start(const std::string& name) { + if (!m_times.contains(name)) { + this->create(name); + } + m_times[name].start = std::chrono::high_resolution_clock::now(); + return *this; + } + + void end(const std::string& name) { + //int idx = idx_map[name]; + if (m_times.contains(name)) { + m_times[name].end = std::chrono::high_resolution_clock::now(); + } + } + + void show() const + { + for (const auto& [name, time] : m_times) { + const auto count = std::chrono::duration_cast(time.end - time.start).count(); + std::cout << "Spent "<< count << " milliseconds on " << name << "\n"; + } + } + }; + + struct TimerGuard { + TimerGuard() = delete; + + TimerGuard(RSRTimer& timer, const std::string& name) : timer(timer), name(name) + { + timer.start(name); + } + + TimerGuard(const TimerGuard& other) = delete; + + ~TimerGuard() + { + timer.end(name); + } + private: + RSRTimer& timer; + std::string name; + }; +} + +#endif //GEL_HMESH_RSRTIMER_H diff --git a/src/GEL/Util/RangeTools.h b/src/GEL/Util/RangeTools.h new file mode 100644 index 00000000..ff60d1b6 --- /dev/null +++ b/src/GEL/Util/RangeTools.h @@ -0,0 +1,398 @@ +// +// Created by Cem Akarsubasi on 9/9/25. +// +// Useful utilities for working with ranges + +#ifndef GEL_RANGETOOLS_H +#define GEL_RANGETOOLS_H + +#include + +// TODO: Cleaner encapsulation of types included here + +/// Useful utilities for working with ranges +/// +/// @details C++20 ranges library is unfortunately fairly incomplete, lacking a large chunk of useful view adapters +/// that are needed for functional style code. +namespace Util::Ranges +{ +template +struct zip_view_t : std::ranges::view_interface> { + using value_type = std::pair, std::ranges::range_value_t>; + using reference = std::pair, std::ranges::range_reference_t>; + +private: +public: + class sentinel_t; + std::ranges::iterator_t m_begin1; + std::ranges::iterator_t m_begin2; + std::ranges::sentinel_t m_end1; + std::ranges::sentinel_t m_end2; + + class iterator_t { + public: + std::ranges::iterator_t ptr1; + std::ranges::iterator_t ptr2; + + public: + using difference_type = ptrdiff_t; + using value_type = std::pair; + constexpr iterator_t() = default; + constexpr explicit iterator_t(auto _ptr1, auto _ptr2) : ptr1(_ptr1), ptr2(_ptr2) {} + + iterator_t& operator++() + { + ++ptr1; + ++ptr2; + return *this; + } + + iterator_t operator++(int) + { + iterator_t retval = *this; + ++(*this); + return retval; + } + + bool operator==(iterator_t other) const + { + return ptr1 == other.ptr1 || ptr2 == other.ptr2; + } + + bool operator==(sentinel_t other) const + { + return ptr1 == other.m_end1 || ptr2 == other.m_end2; + } + + bool operator!=(iterator_t other) const { return !(*this == other); } + + reference operator*() const + { + return {*ptr1, *ptr2}; + } + }; + + class sentinel_t { + public: + std::ranges::sentinel_t m_end1; + std::ranges::sentinel_t m_end2; + friend class iterator_t; + }; + +public: + zip_view_t() = default; + + zip_view_t(R1&& range1, R2&& range2) : + m_begin1(std::ranges::begin(range1)), + m_begin2(std::ranges::begin(range2)), + m_end1(std::ranges::end(range1)), + m_end2(std::ranges::end(range2)) {} + + [[nodiscard]] + auto begin() const -> iterator_t + { + return iterator_t(m_begin1, m_begin2); + } + + [[nodiscard]] + auto end() const -> sentinel_t + { + return sentinel_t(m_end1, m_end2); + } +}; + +namespace detail +{ + using ExampleIter = std::vector; + static_assert(std::forward_iterator::iterator_t>); + static_assert( + std::sentinel_for::sentinel_t, zip_view_t< + ExampleIter, ExampleIter>::iterator_t>); +} + +/// Helper function to create a zip_view_t +/// +/// @related zip_view_t +struct Zip /*: : std::ranges::range_adaptor_closure */ { + template + [[nodiscard]] + constexpr auto operator()(R1&& range1, R2&& range2) const -> zip_view_t + { + return zip_view_t{std::forward(range1), std::forward(range2)}; + } +}; +// FIXME: replace with std::ranges::zip when upgrading to C++23 +static constexpr Zip zip; + +/// Cartesian product of two ranges +template +std::ranges::input_range auto cartesian_product(R1&& range1, R2&& range2) +{ + //using T1 = std::ranges::range_value_t; + //using T2 = std::ranges::range_value_t; + + auto rv1 = range1 | std::views::all; + auto rv2 = range2 | std::views::all; + + auto size1 = std::ranges::size(rv1); + auto size2 = std::ranges::size(rv2); + + auto wide1 = rv1 | std::views::transform([size2](auto e1) { + std::views::iota(0, size2) | std::views::transform([e1](auto _i) { + return e1; + }); + }) | std::views::join; + + auto wide2 = std::views::iota(0, size1) | std::views::transform([rv2](auto _i) { + return rv2; + }) | std::views::join; + + //return wide1 | zip(wide2); + return zip(wide1, wide2); +} + +template +std::ranges::input_range auto repeat_range(R&& range) +{ + return std::views::iota(0ULL) + | std::views::transform([range = std::forward(range)](auto _unused) { return range; }) + | std::views::join; +} + +template +std::ranges::input_range auto repeat(V&& v) +{ + auto range = std::views::iota(0ULL) | std::views::transform([v = std::forward(v)](auto _unused) { return v; }); + return range; +} + +//template +std::ranges::input_range auto shifted_wrapping(std::ranges::input_range auto&& range, size_t m) +{ + auto ranges = std::views::iota(0ULL) | std::views::transform([&](auto _unused) { return std::views::all(range); }); + auto joined = std::views::join(ranges); + auto n = std::ranges::distance(range); + m %= n; + return joined | std::views::drop(m) | std::views::take(n); +} + +template +std::ranges::viewable_range auto slide2_wraparound(Range&& range) +{ + // TODO: figure this out + //auto length = std::ranges::distance(range) + 1; + //auto repeating1 = repeat_range(range); + //auto repeating2 = repeat_range(range); + //return zip(repeating1 | std::views::take(length), repeating2 | std::views::drop(1) | std::views::take(length)); + using Elem = std::remove_cvref_t>; + const auto size = range.size(); + auto ptr = range.begin(); + const auto end = range.end(); + return std::views::iota(0UL, (size > 1) ? size : 0UL) + | std::views::transform([=, &range]([[maybe_unused]] const auto& _i) mutable -> std::tuple { + const auto& current = ptr; + ++ptr; + const auto& next = (ptr == end) ? range.begin() : ptr; + return std::tie(*current, *next); + }); +} + +template +auto select_lazy(const Range& range, Selector&& selector) +{ + return selector | std::views::transform([&range](auto idx) {return range[idx];}); +} + +template +concept GeneratorClosure = + std::copy_constructible && // Need to return I by value + std::copy_constructible && + requires(G g) + { + { g() } -> std::same_as>; + }; + +/// An atrocious clone of Rust's Iterator interface +//template G> +template Generator> +struct Iterator { + // The generator has to be stored so if the Iterator is reused, we can copy construct it. + Generator generator; + explicit Iterator(Generator&& g = Generator()) : generator(std::forward(g)) {} + + struct sentinel_t; + + struct iterator_t { + Generator generator; + // iterator equality does not make much sense for such a generator, but C++ requires us to track this + // meaning we have to store an index + size_t index = 0; + std::optional item = std::nullopt; + using difference_type = ptrdiff_t; + using value_type = Item; + iterator_t() = default; + iterator_t(const iterator_t& rhs) : generator(rhs.generator), index(rhs.index), item(rhs.item) {} + iterator_t& operator=(const iterator_t& rhs) = default; + iterator_t(iterator_t&& rhs) noexcept = default; //: generator(rhs.generator), index(rhs.index), item(rhs.item) {} + iterator_t& operator=(iterator_t&& rhs) = default; + explicit iterator_t(const Generator& generator) : generator(generator), item(std::nullopt) + { + // Since advancing the iterator and fetching elements are different operations + // we need to initialize the item + item = this->generator(); + } + + iterator_t& operator++() + { + index++; + item = generator(); + return *this; + } + + iterator_t operator++(int) + { + iterator_t retval = *this; + ++(*this); + return retval; + } + + bool operator==(iterator_t other) const + { + return index == other.index; + } + + bool operator==([[maybe_unused]] sentinel_t other) const + { + return !item.has_value(); + } + + bool operator!=(iterator_t other) const { return !(*this == other); } + + value_type operator*() const + { + return *item; + } + }; + // A sentinel can be entirely empty, since whether we hit the end depends on whether the closure returns further + // items + struct sentinel_t {}; + + auto begin() -> iterator_t + { + return iterator_t(generator); + } + + auto end() -> sentinel_t + { + return sentinel_t{}; + } + + static_assert(std::input_iterator); + static_assert(std::sentinel_for); +}; + +namespace detail +{ + struct DummyGenerator { + std::optional operator()() + { + return std::nullopt; + } + }; + struct FibonacciGenerator { + std::uint64_t n0 = 0; + std::uint64_t n1 = 1; + + std::optional operator()() + { + const auto temp = n0 + n1; + n0 = n1; + n1 = temp; + return n0 + n1; + } + }; + inline void test() + { + auto it = Iterator(FibonacciGenerator()); + for (auto fib : it | std::views::take(50)) { + std::cout << fib << '\n'; + } + } + using ExampleIter = std::vector; + static_assert(std::forward_iterator::iterator_t>); + static_assert( + std::sentinel_for::sentinel_t, Iterator::iterator_t>); +} + +/// Create an infinite generator using the provided initial state and fold function (State -> State) +template requires std::same_as, std::remove_cvref_t> +std::ranges::viewable_range auto make_generator(State&& initial, Functor&& func) +{ + // For some reason, without the explicit remove_cvref_t, this closure returns a reference to "current" rather + // than returning it by value + auto generator = [state = initial, &func]([[maybe_unused]] int _unused) mutable -> std::remove_cvref_t { + auto current = state; + state = func(current); + return current; + }; + return std::views::iota(0) | std::views::transform(generator); +} + +/// Create an infinite generator using the provided initial state and fold function (State -> optional State) +// template //requires std::same_as, std::remove_cvref_t> +// std::ranges::range auto make_generator_finite(State&& initial, Functor&& func) +// { +// // using StateVal = std::remove_cvref_t; +// // struct Generator { +// // StateVal initial; +// // StateVal current; +// // +// // std::optional> operator()() +// // { +// // if (current.has_value()) +// // current = func(current.value()); +// // return current; +// // } +// // }; +// // auto generator = [state = initial, &func]() mutable -> std::optional> { +// // std::optional> current = state; +// // if (current.has_value()) +// // state = func(current.value()); +// // return current; +// // }; +// // return Iterator, decltype(generator)>(std::move(generator)); +// } + +// struct iterator_t { +// using difference_type = ptrdiff_t; +// using value_type = Item; +// iterator_t() = default; +// iterator_t(const iterator_t& rhs) = default; +// iterator_t& operator=(const iterator_t& rhs) = default; +// iterator_t(iterator_t&& rhs) noexcept = default; +// iterator_t& operator=(iterator_t&& rhs) = default; +// explicit iterator_t(...): ... {} +// +// iterator_t& operator++() +// { +// index++; +// return *this; +// } +// +// iterator_t operator++(int) +// { +// iterator_t retval = *this; +// ++(*this); +// return retval; +// } +// +// bool operator==(iterator_t other) const +// { +// return /**/; +// } +// }; + +} + + +#endif //GEL_RANGETOOLS_H diff --git a/src/GEL/Util/RawObj.cpp b/src/GEL/Util/RawObj.cpp new file mode 100644 index 00000000..e4a91295 --- /dev/null +++ b/src/GEL/Util/RawObj.cpp @@ -0,0 +1,325 @@ +// +// Created by Cem Akarsubasi on 5/22/25. +// + +#include // MSVC: getline, stod +#include +#include + +#include + +#include + +namespace Util +{ +using namespace Combinators; + +std::ostream& operator<<(std::ostream& os, const RawObj& obj) +{ + for (const auto& v : obj.vertices) { + os << "v " << v[0] << " " << v[1] << " " << v[2] << "\n"; + } + for (const auto& vn : obj.normals) { + os << "vn" << vn[0] << " " << vn[1] << " " << vn[2] << "\n"; + } + for (const auto& vt : obj.texture_coordinates) { + os << "vt" << vt[0] << " " << vt[1] << "\n"; + } + for (const auto& face : obj.faces) { + os << "f "; + for (const auto& triplet : face) { + os << triplet.vertex_id; + if (triplet.normal_id || triplet.tcoord_id) { + os << "/"; + if (triplet.tcoord_id) { + os << *triplet.tcoord_id; + } + if (triplet.normal_id) { + os << "/" << *triplet.normal_id; + } + } + + os << " "; + } + os << "\n"; + } + return os; +} + +std::istream& operator>>(std::istream& is, RawObj& obj) +{ + obj.vertices.clear(); + std::string line; + while (auto& more = std::getline(is, line)) { + std::string_view line_view = line; + if (auto pos = parse_prefix_then_float_triplet("v", line_view)) { + obj.vertices.emplace_back(*pos); + } + if (auto pos = parse_prefix_then_float_triplet("vn", line_view)) { + obj.normals.emplace_back(*pos); + } + if (auto pos = parse_prefix_then_float_triplet("vt", line_view)) { + obj.normals.emplace_back(*pos); + } + if (auto face = parse_face_element(line_view)) { + obj.faces.push_back(std::move(face->triplets)); + } + } + return is; +} + +namespace Combinators +{ + bool parse_one(const char c, std::string_view& s) + { + if (s.empty()) { + return false; + } else if (s.front() == c) { + s = s.substr(1); + return true; + } + return false; + } + + bool parse_string(const std::string_view& fragment, std::string_view& s) + { + if (s.starts_with(fragment)) { + s = s.substr(fragment.length()); + return true; + } else { + return false; + } + } + + bool ignore_spaces(std::string_view& s) + { + const auto pos = s.find_first_not_of(' '); + if (pos != std::string::npos) { + s = s.substr(pos); + } else { + s = s.substr(s.length()); + } + return true; + } + + /// In a far away not-so-dark future, we will eventually have a floating point parser in the standard library + /// With sane behavior (no ignoring whitespace or accepting leading + signs), + /// Proper error reporting (no errno or exceptions) + /// Supported by all compilers. + /// + /// Today is not that day, and as it turns out, while from_chars support can be tested, + /// some compilers can claim to support it while not supporting it for floating point values. + +#if !defined(__clang__) && !defined(_MSC_VER) + std::optional parse_float(std::string_view& s) + { + double out; + const auto begin = s.begin(); + const auto end_pos = s.find(' '); + const auto end = (end_pos == std::string::npos) ? s.end() : s.begin() + end_pos; + const auto [p, ec] = std::from_chars(begin, end, out); + if (ec != std::errc()) { + return std::nullopt; + } else { + const auto offset = p - begin; + s = s.substr(offset); + return out; + } + } + + /// FIXME: Check if we live in that future where this code can be deleted +#else + std::optional parse_float(std::string_view& s) + { + const auto begin = s.begin(); + const auto end_pos = s.find(' '); + if (end_pos == 0) { + return std::nullopt; + } + const auto end = (end_pos == std::string::npos) ? s.end() : s.begin() + end_pos; + // we need to make a copy because stod and strtod need a null terminated string + // and a string view is not null terminated + const auto copy = std::string(begin, end); + try { + size_t end_point = 0; + double out = std::stod(copy, &end_point); + if (end_point == 0) { + return std::nullopt; + } + s = s.substr(end_point); + return out; + } catch (std::exception& ex) { + return std::nullopt; + } + } +#endif + + std::optional parse_uint(std::string_view& s) + { + std::uint64_t out; + // Due to MSVC lacking the relevant overloads wrt from_chars and string_view, we have to use this awkward + // juggling of data() and length() instead of begin() and end(). + const auto begin = s.data(); + const auto end_pos = s.find(' '); + const auto end = (end_pos == std::string_view::npos) ? s.data() + s.length() : s.data() + end_pos; + const auto [p, ec] = std::from_chars(begin, end, out); + if (ec != std::errc()) { + return std::nullopt; + } else { + const auto offset = p - begin; + s = s.substr(offset); + return out; + } + } + + std::optional parse_face_triplet(std::string_view& s) + { + auto s_temp = s; + const auto out1 = parse_uint(s_temp); + if (!out1) return std::nullopt; + // guaranteed to have at least one other thing + if (parse_one('/', s_temp)) { + const auto out2 = parse_uint(s_temp); + parse_one('/', s_temp); + const auto out3 = parse_uint(s_temp); + ignore_spaces(s_temp); + s = s_temp; + return FaceTriplet{*out1, out2, out3}; + } else { + // Only vertices + ignore_spaces(s_temp); + s = s_temp; + return FaceTriplet{*out1, std::nullopt, std::nullopt}; + } + } + + std::optional parse_float_ws(std::string_view& s) + { + if (const auto d = parse_float(s)) { + ignore_spaces(s); + return d; + } else { + return std::nullopt; + } + } + + std::optional parse_float_doublet_ws(std::string_view& s) + { + auto s_temp = s; + const auto out1 = parse_float_ws(s_temp); + if (!out1) return std::nullopt; + const auto out2 = parse_float_ws(s_temp); + if (!out2) return std::nullopt; + s = s_temp; + return CGLA::Vec2d{*out1, *out2}; + } + + std::optional parse_float_triplet_ws(std::string_view& s) + { + auto s_temp = s; + const auto out1 = parse_float_ws(s_temp); + if (!out1) return std::nullopt; + const auto out2 = parse_float_ws(s_temp); + if (!out2) return std::nullopt; + const auto out3 = parse_float_ws(s_temp); + if (!out3) return std::nullopt; + s = s_temp; + return CGLA::Vec3d{*out1, *out2, *out3}; + } + + std::optional parse_prefix_then_float_doublet(const std::string_view prefix, std::string_view& s) + { + auto s_temp = s; + if (!parse_string(prefix, s_temp)) return std::nullopt; + ignore_spaces(s_temp); + auto pos = parse_float_doublet_ws(s_temp); + if (pos) { + s = s_temp; + return pos; + } else { + return std::nullopt; + } + } + + std::optional parse_prefix_then_float_triplet(const std::string_view prefix, std::string_view& s) + { + auto s_temp = s; + if (!parse_string(prefix, s_temp)) return std::nullopt; + ignore_spaces(s_temp); + auto pos = parse_float_triplet_ws(s_temp); + if (pos) { + s = s_temp; + return pos; + } else { + return std::nullopt; + } + } + + std::optional parse_face_element(std::string_view& s) + { + auto s_temp = s; + if (!parse_string("f", s_temp)) return std::nullopt; + ignore_spaces(s_temp); + std::vector triplets; + while (auto triplet_maybe = parse_face_triplet(s_temp)) { + triplets.push_back(*triplet_maybe); + } + s = s_temp; + return FaceElement{std::move(triplets)}; + } +} + +std::optional read_raw_obj(const std::filesystem::path& file_path) +{ + std::ifstream file(file_path); + if (!file.is_open()) return std::nullopt; + RawObj obj; + file >> obj; + if (file.bad()) return std::nullopt; + return obj; +} + + +void write_raw_obj(const std::filesystem::path& file_path, const RawObj& obj) +{ + // TODO: error reporting + std::ofstream file(file_path); + file << obj << std::endl; + file.close(); +} + +TriangleMesh to_triangle_mesh(const RawObj& obj) +{ + auto vertices = obj.vertices; + std::vector indices; + for (auto& face : obj.faces) { + // indices start from one + for (auto& vertex : face) { + indices.push_back(vertex.vertex_id - 1); + } + } + auto normals = [&] { + if (!obj.normals.empty()) { + // The overly specific edge case where there are as many normals as vertices + // but there are no faces + if (obj.faces.empty() && obj.normals.size() == obj.vertices.size()) { + return obj.normals; + } + std::vector normals(vertices.size(), CGLA::Vec3d(0.0)); + for (auto& face : obj.faces) { + // indices start from one + for (auto& vertex : face) { + normals.at(vertex.vertex_id - 1) += vertex.normal_id + ? obj.normals.at(*vertex.normal_id - 1) + : CGLA::Vec3d(0.0); + } + } + return normals; + } else { + return std::vector(); + } + }(); + + return TriangleMesh{std::move(vertices), std::move(normals), std::move(indices)}; +} +} diff --git a/src/GEL/Util/RawObj.h b/src/GEL/Util/RawObj.h new file mode 100644 index 00000000..e34c0e73 --- /dev/null +++ b/src/GEL/Util/RawObj.h @@ -0,0 +1,88 @@ +// +// Created by Cem Akarsubasi on 5/22/25. +// +#ifndef RAWOBJ_H +#define RAWOBJ_H + + +#include +#include +#include + +#include +#include + +namespace Util +{ + +/// Parser combinators for easy and low-vulnerability parsing +/// +namespace Combinators +{ + bool parse_one(char c, std::string_view& s); + + bool parse_string(const std::string_view& fragment, std::string_view& s); + + bool ignore_spaces(std::string_view& s); + + std::optional parse_uint(std::string_view& s); + + struct FaceTriplet { + std::uint64_t vertex_id = -1; + std::optional tcoord_id; + std::optional normal_id; + }; + + std::optional parse_face_triplet(std::string_view& s); + + std::optional parse_float(std::string_view& s); + + std::optional parse_float_ws(std::string_view& s); + + std::optional parse_float_doublet_ws(std::string_view& s); + + std::optional parse_float_triplet_ws(std::string_view& s); + + std::optional parse_prefix_then_float_doublet(std::string_view prefix, std::string_view& s); + + std::optional parse_prefix_then_float_triplet(std::string_view prefix, std::string_view& s); + + struct FaceElement { + std::vector triplets; + }; + + std::optional parse_face_element(std::string_view& s); +} + +/// Raw Wavefront Obj type for easy import/export +/// +struct RawObj { + std::vector vertices; + std::vector normals; + std::vector texture_coordinates; + + using Index = std::uint64_t; + static constexpr Index InvalidIndex = std::numeric_limits::max(); + // TODO: + // Note: this should probably use smallvec internally for up to 4 vertices + std::vector> faces; + + friend std::ostream& operator<<(std::ostream& os, const RawObj& obj); + friend std::istream& operator>>(std::istream& is, RawObj& obj); +}; + +struct TriangleMesh { + std::vector vertices; + std::vector normals; + std::vector indices; +}; + +TriangleMesh to_triangle_mesh(const RawObj& obj); + +std::optional read_raw_obj(const std::filesystem::path& file_path); + +void write_raw_obj(const std::filesystem::path& file_path, const RawObj& obj); + +} + +#endif //RAWOBJ_H diff --git a/src/GEL/Util/SparseGraph.h b/src/GEL/Util/SparseGraph.h new file mode 100644 index 00000000..f92afc00 --- /dev/null +++ b/src/GEL/Util/SparseGraph.h @@ -0,0 +1,265 @@ +// +// Created by Cem Akarsubasi on 8/29/25. +// + +#ifndef GEL_FLATGRAPH_H +#define GEL_FLATGRAPH_H + +#include +#include +#include +#include +#include +#include + +//#include +//#include + +namespace Util +{ + + +template +class SparseGraph { +public: + using NodeID = size_t; + //using EdgeID = size_t; + + struct NodeIDPair { + NodeID from; + NodeID to; + + std::strong_ordering operator<=>(NodeIDPair other) const noexcept + { + return std::tie(from, to) <=> std::tie(other.from, other.to); + } + + std::weak_ordering operator<=>(NodeID other) const noexcept { return from <=> other; } + }; + + struct edge_t { + NodeID from; + NodeID to; + EdgeValue key; + }; + + struct graph_size_t { + NodeID nodes; + NodeID edges; + }; + + struct equal_t { + using is_transparent = std::true_type; + + bool operator()(edge_t const& lhs, edge_t const& rhs) const noexcept + { + return lhs.from == rhs.from && lhs.to == rhs.to; + } + + bool operator()(NodeID lhs, edge_t const& rhs) const noexcept { return lhs == rhs.from; } + bool operator()(edge_t const& lhs, NodeID rhs) const noexcept { return lhs.from == rhs; } + + bool operator()(edge_t const& lhs, NodeIDPair const& rhs) const noexcept + { + return lhs.from == rhs.from && lhs.to == rhs.to; + } + + bool operator()(NodeIDPair const& lhs, edge_t const& rhs) const noexcept + { + return lhs.from == rhs.from && lhs.to == rhs.to; + } + }; + + using compare_t = std::less<>; + + struct hash_t { + using is_transparent = std::true_type; + std::size_t operator()(NodeID from) const noexcept { return std::hash{}(from); } + std::size_t operator()(edge_t const& edge) const noexcept { return std::hash{}(edge.from); } + + std::size_t operator()(NodeIDPair const& pair) const noexcept + { + return std::hash{}(pair.from); + } + }; + + /// ID type for edges + //using EdgeID = size_t; + + /// Special ID value for invalid node + static constexpr NodeID InvalidNodeID = std::numeric_limits::max(); + + /// Special ID value for invalid edge + static constexpr std::nullopt_t InvalidEdgeID = std::nullopt; + + +private: + using Key = NodeIDPair; + + using allocator_type_ = std::allocator; + using map_allocator_type = typename std::allocator_traits::template rebind_alloc; + + /// The array containing the actual nodes + //Util::HashSet m_edges; + Util::BTreeMap m_edges; + size_t no_nodes_created = 0; + + /// Number of edges. + +public: + /// Clear the graph + void clear() + { + m_edges.clear(); + no_nodes_created = 0; + } + + void reserve(size_t capacity) + { + m_edges.reserve(capacity * 2); + } + + graph_size_t size() const noexcept + { + graph_size_t size; + size.edges = m_edges.size(); + size.nodes = 0; + for (auto const& edge : m_edges) size.nodes = std::max(size.nodes, edge.from); + return size; + } + + /** Return number of nodes. Note that nodes are never removed from edge_map, so the number of nodes is not decremented. + If you use the derived class AMGraph3D and call cleanup, the number of nodes will be the actual number. */ + // FIXME: just return the actual number + size_t no_nodes() const { return no_nodes_created; } + + /** Return number of edges created. Note that if nodes were disconnected thereby removing an edge, + this number is not decremented. If you use the derived class AMGraph3D and call cleanup, the number + of edges will be the actual number. */ + [[nodiscard]] + size_t no_edges() const { return m_edges.size() / 2; } + + /// Return whether the node is valid (i.e. in the graph) + // FIXME: just return the actual validity + // bool valid_node_id(NodeID n) const { return n < no_nodes();} + + /// Return whether an edge is valid (i.e. in the graph) + // // FIXME: just return the actual validity + // bool valid_edge_id(EdgeID e) const { return e < no_edges_created;} + + /// Returns true if the graph contains no nodes, false otherwise + [[nodiscard]] + bool empty() const { return no_nodes() == 0; } + + /// Add a node to the graph + NodeID add_node() + { + auto id = no_nodes_created; + no_nodes_created += 1; + return id; + } + + void connect_nodes(NodeID from, NodeID to, EdgeValue value) noexcept(false) + { + if (from == to) return; // Skip self-loop + + m_edges.emplace(Key {from, to}, value); + m_edges.emplace(Key {to, from}, value); + } + + std::optional find_edge(NodeID from, NodeID to) const noexcept + { + if (auto it = m_edges.find({from, to}); it != m_edges.end()) return it->second; //it->key; + return std::nullopt; + } + + void remove_edge(NodeID from, NodeID to) noexcept + { + m_edges.erase(Key (from, to)); + m_edges.erase(Key (to, from)); + } + + void compact() noexcept(false) + { + // Erasing does not trigger a rehash, so we do it manually: + m_edges.rehash(0); + } + + std::ranges::input_range auto all_edges_lazy() const noexcept + { + return std::ranges::subrange(m_edges.cbegin(), m_edges.cend()) + | std::views::filter([](const auto& edge) -> bool { return edge.first.from < edge.first.to; }); + //| std::views::filter([](const Key& edge) -> bool { return edge.from < edge.to; }); + } + + std::ranges::input_range auto edges_lazy(NodeID from) const noexcept + { + auto [begin, end] = m_edges.equal_range(from); + return std::ranges::subrange(begin, end); + } + + std::ranges::input_range auto neighbors_lazy(NodeID from) const noexcept + { + return edges_lazy(from) | std::views::keys | std::views::transform([](const Key& edge) { return edge.to; }); + } + + [[nodiscard]] std::ranges::input_range + auto shared_neighbors_lazy(const NodeID n0, const NodeID n1) const + { + auto neighbors0 = neighbors_lazy(n0); + //auto neighbors1 = neighbors_lazy(n1); + return neighbors0 + | std::views::filter([this, n1](const NodeID& n0_n) { return m_edges.contains(NodeIDPair{n0_n, n1}); }); + } + + template + void for_edges(NodeID from, visitor_type_ visitor) const noexcept + { + for (auto& edge : edges_lazy(from)) { visitor(edge.from, edge.to, edge.weight); } + } + + /// The range returned can be used in range based for loops over all node ids + const Util::Range node_ids() const { return Util::Range(0, no_nodes_created);} + // + // /// The range returned can be used in range based for loops over all node ids + // const Util::Range edge_ids() const { return Util::Range(0,no_edges_created);} + + /// Return the number of edges incident on a given node. + size_t valence(NodeID n) const { return std::ranges::distance(edges_lazy(n)); } +}; + +template +std::vector::NodeID>> +connected_components(const SparseGraph& g, const HashSet::NodeID>& s) { + using NodeID = SparseGraph::NodeID; + using NodeSet = HashSet::NodeID>; + using NodeQueue = std::queue; + + NodeSet s_visited; + std::vector component_vec; + for(auto nf0 : s) { + if(s_visited.count(nf0)==0) + { + NodeQueue Q; + Q.push(nf0); + s_visited.insert(nf0); + NodeSet component; + while(!Q.empty()) + { + NodeID nf = Q.front(); + Q.pop(); + component.insert(nf); + for(auto nnf: g.neighbors_lazy(nf)) + if(s.count(nnf) >0 && s_visited.count(nnf)==0) { + Q.push(nnf); + s_visited.insert(nnf); + } + } + component_vec.push_back(component); + } + } + return component_vec; +} +} + +#endif //GEL_FLATGRAPH_H diff --git a/src/GEL/Util/ThreadPool.h b/src/GEL/Util/ThreadPool.h new file mode 100644 index 00000000..5618715d --- /dev/null +++ b/src/GEL/Util/ThreadPool.h @@ -0,0 +1,223 @@ +// +// Created by Cem Akarsubasi on 4/25/25. +// + +#ifndef GEL_UTIL_THREADPOOL_H +#define GEL_UTIL_THREADPOOL_H + +#include +#include +#include +#include +#include +#include + +namespace Util +{ + +class IExecutor { +public: + virtual ~IExecutor() = default; + + [[nodiscard]] + virtual size_t size() const = 0; + + virtual void addTask(std::function&& task) = 0; + + virtual void waitAll() = 0; +}; + +class ImmediatePool final : public IExecutor { + using thread_t = std::thread; + size_t m_size; + std::vector m_threads; +public: + explicit ImmediatePool(const size_t size = std::thread::hardware_concurrency()) : m_size{size} {} + ~ImmediatePool() override + { + waitAll(); + } + + [[nodiscard]] size_t size() const override + { + return m_size; + } + + void addTask(std::function&& task) override + { + m_threads.emplace_back(task); + } + + void waitAll() override + { + for (auto& thread : m_threads) { + if (thread.joinable()) { + thread.join(); + } + } + } +}; + +/// TODO: fix the remaining race condition that seem to only trigger on ARM +/// TODO: maybe just use std::thread since we don't even rely on jthread + +/// Apple Clang does not support jthread without an additional argument +/// We don't rely meaningfully on jthread, so a C+11 thread fallback is included + +/// @brief a non-generic threadpool implementation +class ThreadPool final : public IExecutor { +#if defined(__APPLE__) + using thread_t = std::thread; +#else + using thread_t = std::jthread; +#endif + + std::mutex m_queue_mutex; + std::counting_semaphore<> m_queue_semaphore{0}; + + std::atomic_int m_number_working = 0; + std::binary_semaphore m_number_working_condition{0}; + + std::queue> m_function_queue; + std::vector m_threads; + +#if defined(__APPLE__) + std::atomic_bool m_should_stop{false}; +#endif + +public: + ThreadPool() = delete; + + /// A Threadpool should never be copied + ThreadPool(const ThreadPool&) = delete; + ThreadPool& operator=(const ThreadPool&) = delete; + + /// @brief Construct a threadpool with the given thread count + /// + /// @throws std::invalid_argument if thread_count is 0 + explicit ThreadPool(const uint32_t thread_count) : m_threads{ + std::vector(thread_count)} + { + if (thread_count == 0) { + throw std::invalid_argument("thread_count must be greater than 0"); + } + for (auto& thread : m_threads) { +#if !defined(__APPLE__) + thread = thread_t([this](const std::stop_token& stop_token) { + while (!stop_token.stop_requested()) { +#else + thread = thread_t([this]() { + while (!m_should_stop.load()) { +#endif + std::function task; + { + while (m_function_queue.empty() && +#if !defined(__APPLE__) + !stop_token.stop_requested() +#else + !m_should_stop.load() +#endif + ) + m_queue_semaphore.acquire(); + std::lock_guard lock(m_queue_mutex); + + if (m_function_queue.empty()) { + continue; + } + task = std::move(m_function_queue.front()); + m_function_queue.pop(); + } + // TODO: exception handling to prevent a thread from going down + std::invoke(task); + + { + // Only one worker thread should wake up the main thread. Only one worker thread will observe + // this atomic variable as 1. + if (m_number_working.fetch_sub(1) == 1) { + m_number_working_condition.release(); + } + } + } + }); + } + } + + ~ThreadPool() override + { + this->cancelAll(); + for (auto& thread : m_threads) { + if (thread.joinable()) { + thread.join(); + } + } + } + + /// @brief number of threads + /// @return number of threads + [[nodiscard]] size_t size() const override + { + return m_threads.size(); + } + + /// @brief Adds a task to the queue + /// @param task task to be executed + void addTask(std::function&& task) override + { + m_number_working.fetch_add(1); + { + std::lock_guard lock(m_queue_mutex); + m_function_queue.push(task); + } + m_queue_semaphore.release(); + } + + /// @brief Waits until all threads have finished + void waitAll() override + { + while (m_number_working.load() != 0) + m_number_working_condition.acquire(); + } + + + /// @brief Cancels every thread + /// + /// After this function, no other tasks should be added + void cancelAll() + { +#if !defined(__APPLE__) + for (auto& t : m_threads) { + t.request_stop(); + } +#else + m_should_stop.store(true); +#endif + m_queue_semaphore.release(static_cast(this->size())); + } +}; + +/// A facade of ThreadPool for trivially running parallel algorithms in one thread +class DummyPool final : public IExecutor +{ +public: + DummyPool() = default; + + [[nodiscard]] + size_t size() const override + { + return 1; + } + + void addTask(std::function&& task) override + { + task(); + } + + void waitAll() override + { + + } +}; + +} // namespace GEL::Util + +#endif // GEL_UTIL_THREADPOOL_H diff --git a/src/demo/RotationSystemReconstruction/rsr.cpp b/src/demo/RotationSystemReconstruction/rsr.cpp index f1e8981e..2a9328f7 100644 --- a/src/demo/RotationSystemReconstruction/rsr.cpp +++ b/src/demo/RotationSystemReconstruction/rsr.cpp @@ -1,6 +1,9 @@ #include -#include "GEL/HMesh/RsR.h" -#include "GEL/HMesh/obj_save.h" +#include +#include + +using Point = CGLA::Vec3d; +using Vector = CGLA::Vec3d; struct PointCloud { std::vector vertices; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 0546d57e..e4d3d142 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -27,6 +27,8 @@ FetchContent_GetProperties(doctest SOURCE_DIR doctest_source_dir) set(DOCTEST_INCLUDE_DIR ${doctest_source_dir}/doctest CACHE INTERNAL "Path to include folder for doctest") # # Add doctest to a project with: # target_include_directories(some_executable PRIVATE ${DOCTEST_INCLUDE_DIR}) + +# include the doctest_discover_tests() script include(${doctest_source_dir}/scripts/cmake/doctest.cmake) add_subdirectory(CGLA-covariance) @@ -36,6 +38,9 @@ add_subdirectory(CGLA-vec) add_subdirectory(Geometry-kdtree) add_subdirectory(Util-Assert) add_subdirectory(Manifold) + +add_subdirectory(RsR) +add_subdirectory(Util-ThreadPool) # The following two rely on key input in order to exit #add_subdirectory(CGLA-ogl) #add_subdirectory(GLGraphics-console) diff --git a/src/test/Manifold/Manifold_test.cpp b/src/test/Manifold/Manifold_test.cpp index 2d42ff16..4c8e4e48 100644 --- a/src/test/Manifold/Manifold_test.cpp +++ b/src/test/Manifold/Manifold_test.cpp @@ -32,7 +32,7 @@ struct PointHash { HMesh::VertexID find_vertex_id(const HMesh::Manifold& m, const CGLA::Vec3d& p) { for (size_t i = 0; i < m.no_vertices(); ++i) { - if (m.positions[HMesh::VertexID(i)] == CGLA::Vec3d(0.0, 0.0, 0.0)) + if (m.positions[HMesh::VertexID(i)] == p) return HMesh::VertexID(i); } return HMesh::InvalidVertexID; @@ -538,45 +538,46 @@ TEST_SUITE("Misc.") // Final validation. CHECK(HMesh::valid(m)); } - TEST_CASE("Strange fin") - { - // We start by forming a mesh consisting of two triangles. - std::vector pts = { - CGLA::Vec3d(0,0,0), - CGLA::Vec3d(0,1,0), - CGLA::Vec3d(-1,0,0), - CGLA::Vec3d(1,0,0) - }; - auto m = HMesh::Manifold(); - auto f0 = m.add_face({pts[0],pts[1], pts[2]}); - auto f1 = m.add_face({pts[1],pts[0], pts[3]}); - HMesh::stitch_mesh(m, 1e-6); - // THere must be a single connected components - CHECK_EQ(HMesh::connected_components(m).size(), 1); - // one boundary curve - CHECK_EQ(HMesh::count_boundary_curves(m), 1); - // Final validation. - CHECK(HMesh::valid(m)); - - // Now extrude the shared edge. - HMesh::HalfEdgeID h0; - for (auto h: m.incident_halfedges(f0)) { - if (m.walker(h).opp().face() != HMesh::InvalidFaceID) { - h0 = h; - break; - } - } - HMesh::HalfEdgeSet hset = {h0}; - auto extruded_faces = HMesh::extrude_halfedge_set(m, hset); - auto f = *(extruded_faces.begin()); - // which we cannot merge since it would result in valence 1 vertices. - bool can_we_merge = m.merge_faces(f, h0); - CHECK_FALSE(can_we_merge); - - // Since we have two valence two vertices, we cannot validate that the mesh - // is fully valid since the test is too picky for that. - } + // TEST_CASE("Strange fin") + // { + // // We start by forming a mesh consisting of two triangles. + // std::vector pts = { + // CGLA::Vec3d(0,0,0), + // CGLA::Vec3d(0,1,0), + // CGLA::Vec3d(-1,0,0), + // CGLA::Vec3d(1,0,0) + // }; + // auto m = HMesh::Manifold(); + // auto f0 = m.add_face({pts[0],pts[1], pts[2]}); + // auto f1 = m.add_face({pts[1],pts[0], pts[3]}); + // + // HMesh::stitch_mesh(m, 1e-6); + // // THere must be a single connected components + // CHECK_EQ(HMesh::connected_components(m).size(), 1); + // // one boundary curve + // CHECK_EQ(HMesh::count_boundary_curves(m), 1); + // // Final validation. + // CHECK(HMesh::valid(m)); + // + // // Now extrude the shared edge. + // HMesh::HalfEdgeID h0; + // for (auto h: m.incident_halfedges(f0)) { + // if (m.walker(h).opp().face() != HMesh::InvalidFaceID) { + // h0 = h; + // break; + // } + // } + // HMesh::HalfEdgeSet hset = {h0}; + // auto extruded_faces = HMesh::extrude_halfedge_set(m, hset); + // auto f = *(extruded_faces.begin()); + // // which we cannot merge since it would result in valence 1 vertices. + // bool can_we_merge = m.merge_faces(f, h0); + // CHECK_FALSE(can_we_merge); + // + // // Since we have two valence two vertices, we cannot validate that the mesh + // // is fully valid since the test is too picky for that. + // } } TEST_CASE("Benchmark manifold") diff --git a/src/test/RsR/CMakeLists.txt b/src/test/RsR/CMakeLists.txt new file mode 100644 index 00000000..d76d6705 --- /dev/null +++ b/src/test/RsR/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.30) + +get_filename_component(GEL_INCLUDE_DIR ../.. ABSOLUTE) + +add_executable(RsR_test RsR_test.cpp) +add_executable(OBJParser_test OBJParser_test.cpp) + +set_target_properties(RsR_test OBJParser_test PROPERTIES + CXX_STANDARD 20 + CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined" +) +target_include_directories(RsR_test PRIVATE "${GEL_INCLUDE_DIR}" "${DOCTEST_INCLUDE_DIR}") +target_include_directories(OBJParser_test PRIVATE "${GEL_INCLUDE_DIR}" "${DOCTEST_INCLUDE_DIR}") + +target_link_libraries(RsR_test PRIVATE GEL nanobench) +target_link_libraries(OBJParser_test PRIVATE GEL nanobench) + +doctest_discover_tests(RsR_test TEST_PREFIX "RsR.") +#add_test(NAME GEL.Test.RsR COMMAND RsR_test) +doctest_discover_tests(OBJParser_test TEST_PREFIX "OBJParser.") +#add_test(NAME GEL.Test.OBJParser COMMAND OBJParser_test) + +#add_test(NAME GEL.Test.RsRPython COMMAND ${PYTEST} "${CMAKE_CURRENT_SOURCE_DIR}/rsr.py") +#add_test(NAME GEL.Test.RsRPython COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/rsr.py") diff --git a/src/test/RsR/OBJParser_test.cpp b/src/test/RsR/OBJParser_test.cpp new file mode 100644 index 00000000..12942d9a --- /dev/null +++ b/src/test/RsR/OBJParser_test.cpp @@ -0,0 +1,91 @@ +// +// Created by Cem Akarsubasi on 5/21/25. +// + +#include + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +using namespace Util; +using namespace Util::Combinators; + +static constexpr auto FILE_CAPITAL_A = "../../../../data/PointClouds/Capital_A.obj"; +static constexpr auto FILE_BUNNY_SIMPLE = "../../../../data/bunny.obj"; +static constexpr auto FILE_BUNNY_COMPLEX = "../../../../data/PointClouds/bun_complete.obj"; + +TEST_CASE("ignore_spaces") +{ + constexpr auto test = " a"; + constexpr auto test2 = " "; + + auto view = std::string_view(test); + auto view2 = std::string_view(test2); + SUBCASE("simple") { + CHECK_EQ(ignore_spaces(view), true); + CHECK_EQ(view, "a"); + } + SUBCASE("ignore all") { + CHECK_EQ(ignore_spaces(view2), true); + CHECK_EQ(view2, ""); + } +} + +TEST_CASE("parse_string") +{ + constexpr auto test = "hello"; + auto view = std::string_view(test); + + SUBCASE("simple") { + CHECK_EQ(parse_string("hel", view), true); + CHECK_EQ(view, "lo"); + } +} + +TEST_CASE("parse_float") +{ + constexpr auto test = "1.5"; + auto view = std::string_view(test); + SUBCASE("simple") { + CHECK_EQ(parse_float(view), 1.5); + CHECK_EQ(parse_float(view), std::nullopt); + } + + constexpr auto test2 = " 1.5"; + auto view2 = std::string_view(test2); + SUBCASE("leading space") { + CHECK_EQ(parse_float(view2), std::nullopt); + ignore_spaces(view2); + CHECK_EQ(parse_float(view2), 1.5); + } +} + +TEST_CASE("parse triplet") +{ + constexpr auto test = "v 1.0 0.0 -1.0"; + constexpr auto test2 = "vn 1.0 0.0 -1.0"; + auto view = std::string_view(test); + auto view2 = std::string_view(test2); + SUBCASE("simple") + { + CHECK_EQ(parse_prefix_then_float_triplet("v", view), CGLA::Vec3d(1.0, 0.0, -1.0)); + CHECK_EQ(parse_prefix_then_float_triplet("v", view2), std::nullopt); + } +} + +bool float_eq(const double a, const double b, const float eps = 1.0e-6) +{ + return std::abs(a - b) < eps; +} + +// TEST_CASE("read raw obj") +// { +// auto file_path = "../../../../data/bunny.obj"; +// auto file_path_str = std::string(file_path); +// auto robj = read_raw_obj(file_path); +// auto pc = read_obj(file_path_str); +// CHECK_EQ(robj.vertices.size(), pc.vertices.size()); +// CHECK_EQ(robj.normals.size(), pc.normals.size()); +// +// std::cout << pc.vertices.size() << std::endl; +// } \ No newline at end of file diff --git a/src/test/RsR/RsR_test.cpp b/src/test/RsR/RsR_test.cpp new file mode 100644 index 00000000..0a09ba7f --- /dev/null +++ b/src/test/RsR/RsR_test.cpp @@ -0,0 +1,822 @@ +// +// Created by Cem Akarsubasi on 4/15/25. +// + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +using HMesh::RSR::point_cloud_to_mesh; +using namespace HMesh::RSR; +using namespace CGLA; + +static constexpr auto FILE_CAPITAL_A = "../../../../data/PointClouds/Capital_A.obj"; +static constexpr auto FILE_BUNNY_SIMPLE = "../../../../data/bunny_with_normals.obj"; +static constexpr auto FILE_BUNNY_SIMPLE_NO_NORMALS = "../../../../data/bunny.obj"; +static constexpr auto FILE_BUNNY_COMPLEX = "../../../../data/PointClouds/bun_complete.obj"; +static constexpr auto FILE_THINGY = "../../../../data/thingy.obj"; +static constexpr auto FILE_AS = "../../../../data/as.obj"; + +template +constexpr auto make_array(Args... args) +{ + constexpr auto size = sizeof...(args); + return std::array{args...}; +} + +static constexpr auto QUICK_TEST_FILES = make_array(FILE_CAPITAL_A, FILE_BUNNY_SIMPLE, FILE_THINGY, FILE_AS); +static constexpr auto TEST_FILES = make_array(FILE_CAPITAL_A, FILE_BUNNY_SIMPLE, FILE_BUNNY_COMPLEX, FILE_THINGY, + FILE_AS); + +constexpr auto IS_EUCLIDEAN = false; +constexpr auto K_PARAM = 30; +constexpr auto GENUS = -1; +constexpr auto R_PARAM = 20; +constexpr auto THETA = 60; +constexpr auto N_PARAM = 50; + +auto test_options() +{ + RsROpts opts; + opts.dist = IS_EUCLIDEAN ? Distance::Euclidean : Distance::Tangent; + opts.k = K_PARAM; + opts.genus = GENUS; + opts.r = R_PARAM; + opts.theta = THETA; + opts.n = N_PARAM; + return opts; +} + +template +auto indices_from(const Collection& collection) -> std::vector +{ + const auto indices = [&collection] { + std::vector temp(collection.size()); + std::iota(temp.begin(), temp.end(), 0); + return temp; + }(); + return indices; +} + +template +auto indexed_select(const std::vector& vec, const std::vector& indices) -> std::vector +{ + std::vector result; + result.reserve(indices.size()); + for (auto idx : indices) { + result.push_back(vec.at(idx)); + } + return result; +} + +inline +auto angle_cost(const Vec3d e1, const Vec3d e2) -> double +{ + return std::abs(2.0 * dot(e1, e2) / (e1.length() * e2.length() + 1e-18) - 1.0); +} + +inline +auto edge_cost(const Vec3d e1, const Vec3d e2) -> double +{ + const Vec3d e3 = e1 - e2; + return angle_cost(e1, e2) * e3.length(); +} + +inline +auto triangle_cost(const Vec3d p1, const Vec3d p2, const Vec3d p3) -> double +{ + return edge_cost((p2 - p1), (p3 - p1)) + edge_cost((p1 - p2), (p3 - p2)) + edge_cost((p1 - p3), (p2 - p3)); +} + +auto manifold_cost(const HMesh::Manifold& manifold) -> double +{ + double total_cost = 0.0; + for (const auto f : manifold.faces()) { + std::array arr; + int idx = 0; + HMesh::circulate_face_ccw(manifold, f, [&idx, &arr](const HMesh::VertexID v) { + if (idx < 3) + arr[idx++] = v; + }); + if (idx == 3) { + const auto p1 = manifold.positions[arr[0]]; + const auto p2 = manifold.positions[arr[1]]; + const auto p3 = manifold.positions[arr[2]]; + const auto cost = triangle_cost(p1, p2, p3); + total_cost += cost; + } + } + return total_cost; +} + +auto manifold_average_edge_length(const HMesh::Manifold& manifold) -> double +{ + double total = 0.0; + for (const auto halfedge : manifold.halfedges()) { + HMesh::Walker walker = manifold.walker(halfedge); + auto v1 = walker.vertex(); + auto v2 = walker.opp().vertex(); + GEL_ASSERT_NEQ(v1, v2); + auto p1 = manifold.positions[v1]; + auto p2 = manifold.positions[v2]; + total += std::sqrt(dot((p2 - p1), (p2 - p1))); + } + return total / static_cast(manifold.no_halfedges()); +} + +auto manifold_cost_normalized(const HMesh::Manifold& manifold) -> double +{ + const auto number_of_faces = manifold.no_faces(); + const auto total_cost = manifold_cost(manifold); + + return total_cost / static_cast(number_of_faces) / manifold_average_edge_length(manifold); +} + +auto manifold_is_triangular(const HMesh::Manifold& manifold) -> bool +{ + for (const auto f : manifold.faces()) { + int idx = 0; + HMesh::circulate_face_ccw(manifold, f, [&idx](const HMesh::VertexID v) { + idx++; + }); + if (idx != 3) + return false; + } + return true; +} + +auto manifold_face_vert_ratio(const HMesh::Manifold& manifold) -> double +{ + return static_cast(manifold.no_faces()) / static_cast(manifold.no_vertices()); +} + +/// Estimate quality of a Manifold based on dihedral angles. +/// @details The dihedral angle between each face is used to determine how expensive it would be to "cross" between +/// those two faces. The average of this for every half-edge is used to get the total cost. Boundary edges are considered +/// to be the most expensive to cross. +/// +/// This gives a quality metric in terms of how "smooth" a Manifold is. In an infinite flat plane, this function is 0. +/// However, keep in mind that this is not a generalized quality measure. The Manifold might be intentionally bumpy which +/// will return a high cost. +auto manifold_dihedral_cost(const HMesh::Manifold& manifold) -> std::pair +{ + double total_cost = 0.0; + double edge_scaled_cost = 0.0; + size_t bad_edges = 0; + size_t boundary_edges = 0; + for (const auto half_edge : manifold.halfedges()) { + const auto f1 = manifold.walker(half_edge).face(); + const auto f2 = manifold.walker(half_edge).opp().face(); + if (f1 != HMesh::InvalidFaceID && f2 != HMesh::InvalidFaceID) { + const auto n1 = manifold.normal(f1); + const auto n2 = manifold.normal(f2); + const auto cost = std::abs(CGLA::dot(CGLA::normalize(n1), CGLA::normalize(n2)) - 1.0); + if (!std::isnan(cost)) { + total_cost += cost; + edge_scaled_cost += (cost * manifold.length(half_edge)); + } + else + bad_edges++; + } else { + // boundary edges are considered to be the most expensive + boundary_edges ++; + total_cost += 2.0; + } + } + if (bad_edges > 0) { + std::cerr << "dihedral cost found " << bad_edges << " bad edges\n"; + } + std::cout << "dihedral cost found " << boundary_edges << " boundary edges\n"; + return std::make_pair(total_cost / static_cast(manifold.no_halfedges()), edge_scaled_cost / static_cast(manifold.no_halfedges())); +} + +struct ValencyHistogram { + static constexpr size_t max_tracked_valency = 9; + size_t total = 0; + std::array common_valency{0}; + size_t high = 0; + + friend std::ostream& operator<<(std::ostream& os, const ValencyHistogram& hist); +}; + +std::ostream& operator<<(std::ostream& os, const ValencyHistogram& hist) +{ + os << "Total: " << hist.total << "\n"; + constexpr auto draw_factor = 40; + for (size_t valency = 2; valency < hist.common_valency.size(); ++valency) { + const auto ratio = static_cast(hist.common_valency[valency]) / static_cast(hist.total); + const auto blocks = static_cast(ratio * draw_factor); + os << valency << ": "; + for (int i = 0; i < blocks; ++i) { + os << "#"; + } + for (int i = blocks; i < draw_factor; ++i) { + os << " "; + } + os << " (" << (ratio * 100.0) << "%)\n"; + } + const auto ratio = static_cast(hist.high) / static_cast(hist.total); + const auto blocks = static_cast(ratio * draw_factor); + os << hist.common_valency.size() << "+: "; + for (int i = 0; i < blocks; ++i) { + os << "#"; + } + for (int i = blocks; i < draw_factor; ++i) { + os << " "; + } + os << " (" << (ratio * 100.0) << "%)\n"; + return os; +} + +auto manifold_valency_histogram(const HMesh::Manifold& manifold) -> ValencyHistogram +{ + ValencyHistogram hist; + for (const auto v : manifold.vertices()) { + if (const auto valency = manifold.valency(v); valency > 8) { + hist.high++; + } else { + hist.common_valency[valency]++; + } + } + hist.total = manifold.no_vertices(); + return hist; +} + +auto manifold_valency_histogram_non_boundary(const HMesh::Manifold& manifold) -> ValencyHistogram +{ + ValencyHistogram hist; + size_t non_boundary_vertices = 0; + for (const auto v : manifold.vertices()) { + if (!manifold.boundary(v)) { + non_boundary_vertices++; + if (const auto valency = manifold.valency(v); valency > 8) { + hist.high++; + } else { + hist.common_valency[valency]++; + } + } + } + hist.total = non_boundary_vertices; + return hist; +} + +template +auto collection_all_unique(const Collection& collection) -> bool +{ + using T = typename Collection::value_type; + std::unordered_set set; + for (auto& elem : collection) { + set.insert(elem); + } + return set.size() == collection.size(); +} + +auto manifold_is_identical(const HMesh::Manifold& left, const HMesh::Manifold& right) -> bool +{ + // This is a horrendous way of actually checking if two manifolds are identical, + // but assuming we did not mess something up during construction, they should be + // using identical IDs, which is good enough for quick regression analysis + + const auto left_edges = left.halfedges(); + const auto right_edges = right.halfedges(); + for (auto left_begin = left_edges.begin(), right_begin = right_edges.begin(); + left_begin != left_edges.end() && right_begin != right_edges.end(); + ++left_begin, ++right_begin) { + if (*left_begin != *right_begin) { + return false; + } + } + return true; +} + +template +auto set_is_disjoint(const std::unordered_set& s1, const std::unordered_set& s2) -> bool +{ + return std::ranges::all_of(s1, [&s2](const auto& e) { + return !s2.contains(e); + }); +} + +template +auto set_is_subset_of(const std::unordered_set& super_set, const std::unordered_set& sub_set) -> bool +{ + return std::ranges::all_of(sub_set, [&super_set](const auto& e) { + return super_set.contains(e); + }); +} + +template +auto set_union(const std::unordered_set& s1, const std::unordered_set& s2) -> std::unordered_set +{ + std::unordered_set result; + for (auto& elem : s1) { + result.insert(elem); + } + for (auto& elem : s2) { + result.insert(elem); + } + return result; +} + +template +auto range_is_unique(const Range& range) -> bool +{ + auto filtered = std::ranges::unique_copy(range); + return std::ranges::size(filtered) == std::ranges::size(range); +} + +template +auto all(const Range& range, Func&& f) -> bool +{ + for (auto& elem : range) { + if (!f(elem)) return false; + } + return true; +} + + +// Test functions begin + + +auto test_reconstruct_new(const std::string_view file_name, const RsROpts& opts) -> std::optional +{ + std::cout << "======================\n" + << "Begin new function\n"; + auto input_maybe = Util::read_raw_obj(file_name); + if (!input_maybe) { + WARN_FALSE("File not found."); + return std::nullopt; + } + const auto input = *std::move(input_maybe); + + std::cout << "obj vertices: " << input.vertices.size() << "\n"; + std::cout << "obj normals: " << input.normals.size() << "\n"; + + HMesh::Manifold output = point_cloud_to_mesh(input.vertices, {}, opts); + // k: 70 is too large + // r: needs isEuclidean false + std::cout << output.positions.size() << "\n"; + + return output; +} + +auto point_cloud_to_mesh_legacy(std::vector const& points, const std::vector& normals, const RsROpts& opts) -> HMesh::Manifold +{ + auto points_copy = points; + auto normals_copy = normals; + HMesh::Manifold output; + reconstruct_single(output, points_copy, normals_copy, opts.dist == Distance::Euclidean, opts.genus, opts.k, + opts.r, opts.theta, opts.n); + return output; +} + + +auto test_reconstruct_legacy(const std::string_view file_name, const RsROpts& opts) -> std::optional +{ + std::cout << "======================\n" + << "Begin original function\n"; + auto input_maybe = Util::read_raw_obj(file_name); + if (!input_maybe) { + WARN_FALSE("File not found."); + return std::nullopt; + } + auto input = Util::to_triangle_mesh(*input_maybe); + + // auto collapse_opts = CollapseOpts{}; + // collapse_opts.max_iterations = 1; + // collapse_opts.initial_neighbors = 8; + // collapse_opts.reduction_per_iteration = 0.90; + // auto [_, collapsed] = collapse_points(input.vertices, input.normals, collapse_opts); + + std::cout << "obj vertices: " << input.vertices.size() << "\n"; + std::cout << "obj normals: " << input.normals.size() << "\n"; + + HMesh::Manifold output; + reconstruct_single(output, + //collapsed.points,collapsed.normals, + input.vertices, + input.normals, + opts.dist == Distance::Euclidean, opts.genus, opts.k, + opts.r, opts.theta, opts.n); + // k: 70 is too large + // r: needs isEuclidean false + std::cout << output.positions.size() << "\n"; + + return output; +} + +auto test_reconstruct_collapse_reexpand(const std::string_view file_name, const CollapseOpts& collapse_opts, + const RsROpts& rsr_opts, const ReexpandOptions& reexpand) -> std::optional +{ + std::cout << "======================\n" + << "Begin new function\n"; + auto input_maybe = Util::read_raw_obj(file_name); + if (!input_maybe) { + WARN_FALSE("File not found."); + return std::nullopt; + } + const auto input = Util::to_triangle_mesh(*input_maybe); + + std::cout << "obj vertices: " << input.vertices.size() << "\n"; + std::cout << "obj normals: " << input.normals.size() << "\n"; + + HMesh::Manifold output = point_cloud_collapse_reexpand(input.vertices, input.normals, collapse_opts, rsr_opts, + reexpand); + // k: 70 is too large + // r: needs isEuclidean false + std::cout << output.positions.size() << "\n"; + + return output; +} + +template +Tree build_kd_tree_of_indices(const std::vector& vertices, const Indices& indices) +{ + Tree kd_tree; + for (const auto idx : indices) { + kd_tree.insert(vertices.at(idx), idx); + } + kd_tree.build(); + return kd_tree; +} + +auto reconstruct_assertions(const HMesh::Manifold& manifold) -> void +{ + // Manifold properties: + // Triangular + // Approx. 2 face per vertex (if good reconstruction) + // Each vertex around 6 edges + CHECK_EQ(manifold_is_triangular(manifold), true); + auto ratio = manifold_face_vert_ratio(manifold); + CHECK_GT(ratio, 1.5); + CHECK_LT(ratio, 2.25); + auto faces = manifold.no_faces(); + auto cost = manifold_cost(manifold); + auto average_edge = manifold_average_edge_length(manifold); + auto normed_cost = cost / static_cast(faces) / average_edge; + auto valencies = manifold_valency_histogram(manifold); + auto valencies_nb = manifold_valency_histogram_non_boundary(manifold); + auto [dihedral_cost1, dihedral_cost2] = manifold_dihedral_cost(manifold); + std::cout << "ratio: " << ratio << std::endl; + std::cout << "total cost: " << cost << std::endl; + std::cout << "num faces : " << faces << std::endl; + std::cout << "average edge length: " << average_edge << std::endl; + std::cout << "normed cost: " << normed_cost << std::endl; + std::cout << "Valency histogram (all vertices): " << std::endl; + std::cout << valencies; + std::cout << "Valency histogram (non-boundary vertices): " << std::endl; + std::cout << valencies_nb; + std::cout << "Dihedral cost raw: " << dihedral_cost1 << std::endl; + std::cout << "Dihedral cost adj: " << dihedral_cost2 << std::endl; +} + +template +void test_reconstruct(Func&& f, const bool save, const bool all = false) + requires std::is_same_v(), std::declval())), + std::optional> +{ + const auto test_files = [&] { + if (all) { + return std::ranges::subrange(TEST_FILES); + } else { + return std::ranges::subrange(QUICK_TEST_FILES); + } + }(); + const auto opts = test_options(); + for (const auto file : test_files) { + std::filesystem::path p = file; + auto opts_neighbors = opts; + opts_neighbors.dist = Distance::Tangent; + + auto case_name = p.stem().concat("_neighbors"); + SUBCASE(case_name.c_str()) { + std::optional manifold = f(file, opts_neighbors); + if (manifold.has_value()) { + auto out_path = p.stem().concat("_neighbors").concat(".obj"); + if (save) + HMesh::obj_save(out_path, *manifold); + reconstruct_assertions(*manifold); + } + } + } + for (const auto file : test_files) { + std::filesystem::path p = file; + + auto opts_euclidean = opts; + opts_euclidean.dist = Distance::Euclidean; + + auto case_name = p.stem().concat("_euclidean"); + SUBCASE(case_name.c_str()) { + std::optional manifold = f(file, opts_euclidean); + if (manifold.has_value()) { + auto out_path = p.stem().concat("_euclidean").concat(".obj"); + if (save) + HMesh::obj_save(out_path, *manifold); + reconstruct_assertions(*manifold); + } + } + } +} + +TEST_CASE("reconstruct legacy") +{ + test_reconstruct(test_reconstruct_legacy, false); +} + +TEST_CASE("reconstruct") +{ + test_reconstruct(test_reconstruct_new, true); +} + +TEST_CASE("reconstruct_collapse_reexpand") +{ + auto l = [](T0&& PH1, T1&& PH2) { + return test_reconstruct_collapse_reexpand(std::forward(PH1), CollapseOpts(), std::forward(PH2), ReexpandOptions()); + }; + test_reconstruct(l, true, true); +} + +TEST_CASE("reconstruct_debug") +{ + auto collapse_opts = CollapseOpts{}; + auto opts_euclidean = test_options(); + auto file = FILE_BUNNY_SIMPLE; + collapse_opts.max_iterations = 3; + collapse_opts.initial_neighbors = 8; + collapse_opts.reduction_per_iteration = 0.50; + collapse_opts.max_collapses = 0; + + auto reexpand_opts = ReexpandOptions(); + reexpand_opts.enabled = true; + reexpand_opts.debug_print = false; + reexpand_opts.angle_threshold = std::numbers::pi / 180.0 * 9; + reexpand_opts.angle_threshold_penalty = 0.1; + reexpand_opts.angle_factor *= 1; + reexpand_opts.early_stop_at_error = false; + reexpand_opts.stop_at_error = 0; + reexpand_opts.debug_mask; // |= RE_ERRORS; // | RE_FIRST_FLIP | RE_SECOND_FLIP | RE_ITERATION; + reexpand_opts.stop_at_iteration = 0; + + + opts_euclidean.dist = Distance::Euclidean; + auto manifold = test_reconstruct_collapse_reexpand(file, collapse_opts, opts_euclidean, reexpand_opts); + CHECK(manifold.has_value()); + if (manifold.has_value()) { + HMesh::obj_save("debug_obj_reexpand_euclidean.obj", *manifold); + reconstruct_assertions(*manifold); + } + // auto opts_neighbors = test_options(); + // opts_neighbors.dist = Distance::Tangent; + // manifold = test_reconstruct_collapse_reexpand(file, collapse_opts, opts_neighbors, reexpand_opts); + // CHECK(manifold.has_value()); + // if (manifold.has_value()) { + // HMesh::obj_save("debug_obj_reexpand_neighbors.obj", *manifold); + // reconstruct_assertions(*manifold); + // } +} + +TEST_CASE("reconstruct_legacy_debug") +{ + auto collapse_opts = CollapseOpts{}; + auto opts_euclidean = test_options(); + auto file = FILE_BUNNY_SIMPLE_NO_NORMALS; + + opts_euclidean.dist = Distance::Euclidean; + auto manifold = test_reconstruct_legacy(file, opts_euclidean); + CHECK(manifold.has_value()); + if (manifold.has_value()) { + HMesh::obj_save("debug_obj_reexpand_euclidean_legacy.obj", *manifold); + reconstruct_assertions(*manifold); + } + auto opts_neighbors = test_options(); + opts_neighbors.dist = Distance::Tangent; + manifold = test_reconstruct_legacy(file, opts_neighbors); + CHECK(manifold.has_value()); + if (manifold.has_value()) { + HMesh::obj_save("debug_obj_reexpand_neighbors_legacy.obj", *manifold); + reconstruct_assertions(*manifold); + } +} + +/// Makes the following shape: +/// 1 - 3 +/// / \ / \ +/// 0 - 2 - 4 +/// \ / \ / +/// 5 - 6 +/// +/// @return +HMesh::Manifold make_hex() +{ + std::vector vertices = { + {-2.0, 0.0, 0.0}, + {-1.0, 1.0, 0.0}, + {0.0, 0.0, 0.0}, + {1.0, 1.0, 0.0}, + {2.0, 0.0, 0.0}, + {-1.0, -1.0, 0.0}, + {1.0, -1.0, 0.0} + }; + std::vector faces = { + 2, 0, 1, + 2, 1, 3, + 2, 3, 4, + 2, 5, 0, + 2, 6, 5, + 2, 4, 6, + }; + HMesh::Manifold m; + HMesh::build_manifold(m, vertices, faces, 3); + CHECK_EQ(m.no_faces(), 6); + CHECK_EQ(m.no_vertices(), 7); + CHECK_EQ(m.no_halfedges(), 12 * 2); + return m; +} + +enum Corners { + TOP_LEFT = 0, + TOP_RIGHT = 1, + BOTTOM_LEFT = 2, + BOTTOM_RIGHT = 3, +}; + +struct RectangleHelper { + double gap; + size_t x_size; + size_t y_size; + size_t i; + size_t j; + std::array corners; // tl, tr, bl, br + CGLA::GelPrngDouble& rng_double; + CGLA::GelPrng& rng; +}; + +inline auto place_rectangle(const RectangleHelper& helper, + const double x_begin, + const double x_end, + const double y_begin, + const double y_end) -> bool +{ + const auto x_actual = (double)helper.j / (double)helper.x_size; + const auto y_actual = (double)helper.i / (double)helper.y_size; + return x_actual >= x_begin && x_actual <= x_end && y_actual >= y_begin && y_actual <= y_end; +} + +template requires std::same_as&, RectangleHelper>, + std::optional> +HMesh::Manifold create_rectangular_manifold(const size_t x_size, const size_t y_size, Func&& f) +{ + REQUIRE_GT(x_size, 2); + REQUIRE_GT(y_size, 2); + std::vector faces; + std::vector vertices; + faces.reserve(x_size * y_size * 4); + vertices.reserve((x_size + 1) * (y_size + 1)); + + std::vector dummy_collapses; + + CGLA::GelPrngDouble rng_double; + CGLA::GelPrng rng; + + // for each vertex + for (size_t i = 0; i < y_size + 1; ++i) { + for (size_t j = 0; j < x_size + 1; ++j) { + auto x = 10.0 * static_cast(j) / static_cast(x_size); + auto y = 10.0 * static_cast(i) / static_cast(y_size); + vertices.emplace_back(x, y, 0.0); + } + } + const auto x_gap = (vertices.at(1) - vertices.at(0))[0]; + + std::vector final_remaining(vertices.size()); + std::iota(final_remaining.begin(), final_remaining.end(), 0); + // for each face + for (size_t i = 0; i < y_size; ++i) { + for (size_t j = 0; j < x_size; ++j) { + auto top_left = i * (y_size + 1) + j; // top left + auto top_right = top_left + 1; // top right + auto bottom_left = top_left + 1 + x_size; // bottom left + auto bottom_right = bottom_left + 1; // bottom right + std::array corners{top_left, top_right, bottom_left, bottom_right}; + faces.emplace_back(top_left); + faces.emplace_back(top_right); + faces.emplace_back(bottom_right); + faces.emplace_back(bottom_left); + + auto collapse = f(vertices, RectangleHelper{x_gap, x_size, y_size, i, j, corners, rng_double, rng}); + if (collapse.has_value()) + dummy_collapses.push_back(*collapse); + } + } + + auto collapse = create_collapse(std::move(dummy_collapses)); + HMesh::Manifold m; + HMesh::build_manifold(m, vertices, faces, 4); + triangulate(m); + reexpand_points(m, std::move(collapse), ReexpandOptions()); + + return m; +} + +HMesh::Manifold create_rectangular_manifold_debug(const std::int64_t x_size, const std::int64_t y_size) +{ + return create_rectangular_manifold( + x_size, y_size, + [](const std::vector& vertices, + const RectangleHelper& helper) -> std::optional { + const auto x_shift = static_cast(helper.j + 1) / static_cast( + helper.y_size + 1); + const auto y_shift = static_cast(helper.i + 1) / static_cast( + helper.x_size + 1); + + const auto shift = CGLA::Vec3d{x_shift, y_shift, 0.1} * helper.gap; + const auto random_point = vertices[helper.corners[TOP_LEFT]] + shift; + const auto corner_choice = + (x_shift < 0.5) + ? (y_shift < 0.5) + ? helper.corners[TOP_LEFT] + : helper.corners[BOTTOM_LEFT] + : (y_shift < 0.5) + ? helper.corners[TOP_RIGHT] + : helper.corners[BOTTOM_RIGHT]; + const auto active_coordinates = vertices[corner_choice]; + if (helper.i == 0 || helper.j == 0 || helper.i == helper.y_size - 1 || helper.j == helper.x_size - 1) + return std::nullopt; + if ( + //place_rectangle(helper, 0.60, 0.40, 4) || // fine reexpansion + place_rectangle(helper, 0.45, 0.50, 0.45, 0.50) || + place_rectangle(helper, 0.50, 0.55, 0.45, 0.50) + //place_rectangle(helper, 0.45, 0.55, 6) + ) // wonky reexpansion + { + return SingleCollapse{ + .active_point_coords = active_coordinates, + .latent_point_coords = random_point, + .v_bar = active_coordinates, + }; + } + return std::nullopt; + }); +} + +HMesh::Manifold create_rectangular_manifold_mosaic_closest(const std::int64_t x_size, const std::int64_t y_size) +{ + return create_rectangular_manifold( + x_size, y_size, + [](const std::vector& vertices, + const RectangleHelper& helper) -> std::optional { + const auto x_shift = static_cast(helper.j + 1) / static_cast( + helper.y_size + 1); + const auto y_shift = static_cast(helper.i + 1) / static_cast( + helper.x_size + 1); + + const auto shift = CGLA::Vec3d{x_shift, y_shift, 0.1} * helper.gap; + const auto random_point = vertices[helper.corners[TOP_LEFT]] + shift; + const auto corner_choice = + (x_shift < 0.5) + ? (y_shift < 0.5) + ? helper.corners[TOP_LEFT] + : helper.corners[BOTTOM_LEFT] + : (y_shift < 0.5) + ? helper.corners[TOP_RIGHT] + : helper.corners[BOTTOM_RIGHT]; + const auto active_coordinates = vertices[corner_choice]; + if (helper.i == 0 || helper.j == 0 || helper.i == helper.y_size - 1 || helper.j == helper.x_size - 1) + return std::nullopt; + else + return SingleCollapse{ + .active_point_coords = active_coordinates, + .latent_point_coords = random_point, + .v_bar = active_coordinates, + }; + }); +} + + +TEST_CASE("Rectangular reexpansion") +{ + + auto m = create_rectangular_manifold_debug(50, 50); + HMesh::obj_save("rectangle_debug.obj", m); + auto m2 = create_rectangular_manifold_mosaic_closest(50, 50); + HMesh::obj_save("rectangle.obj", m2); + reconstruct_assertions(m2); + //HMesh::minimize_dihedral_angle(m2); + //std::cout << "Optimized mesh: \n"; + //reconstruct_assertions(m2); + //HMesh::obj_save("rectangle_optimized.obj", m2); +} \ No newline at end of file diff --git a/src/test/Util-ThreadPool/CMakeLists.txt b/src/test/Util-ThreadPool/CMakeLists.txt new file mode 100644 index 00000000..c2276872 --- /dev/null +++ b/src/test/Util-ThreadPool/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.30) + +add_executable(ThreadPool_test ThreadPool_test.cpp) +target_include_directories(ThreadPool_test PRIVATE ../.. ${DOCTEST_INCLUDE_DIR}) +set_target_properties(ThreadPool_test PROPERTIES + CXX_STANDARD 20) +target_link_libraries(ThreadPool_test GEL nanobench) + +#add_test(NAME GEL.Test.ThreadPool COMMAND ThreadPool_test) +doctest_discover_tests(ThreadPool_test TEST_PREFIX "ThreadPool.") \ No newline at end of file diff --git a/src/test/Util-ThreadPool/ThreadPool_test.cpp b/src/test/Util-ThreadPool/ThreadPool_test.cpp new file mode 100644 index 00000000..c48373c9 --- /dev/null +++ b/src/test/Util-ThreadPool/ThreadPool_test.cpp @@ -0,0 +1,376 @@ +// +// Created by arkeo on 4/25/25. +// + +#include +#include +#include +#include +#include +#include + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +#include + +using Util::ThreadPool; +using namespace Util::Parallel; +using namespace Util; + +// bool test_empty() +// { +// ThreadPool pool(2); +// const auto empty = map(pool, std::vector{}, [](auto x) { return x; }); +// return empty.empty(); +// } + +static constexpr auto problem_size = 10000; + +inline float non_trivial_function(const int x) +{ + float total = 0.0; + for (int i = 0; i < 500; ++i) { + total += 1.0f / static_cast(i+1) * std::cos(static_cast(x)/static_cast(i+1)); + } + return total; +} + +bool test_oracle() +{ + constexpr auto size = 10; + ThreadPool pool(3); + std::vector v(size); + std::vector vout(size); + std::vector vout2(size); + std::iota(v.begin(), v.end(), 0); + std::ranges::transform(v.cbegin(), v.cend(), vout.begin(), non_trivial_function); + const auto right = map(pool, v, std::move(vout2), non_trivial_function); + CHECK_EQ(vout, right); + return true; +} + +// TEST_CASE("Destructor doesn't block") +// { +// ThreadPool pool(2); +// } +// +// TEST_CASE("Inert waitAll") +// { +// ThreadPool pool(2); +// pool.waitAll(); +// } + +// TODO: maybe reenable them in the future +// TEST_CASE("deadlock_test1") +// { +// for (int i = 0; i < 100000; ++i) { +// CHECK(test_empty()); +// //std::cout << "DT1 " << i << "\n"; +// } +// } +// +// TEST_CASE("deadlock_test2") +// { +// for (int i = 0; i < 100000; ++i) { +// test_oracle(); +// //std::cout << "DT2 " << i << "\n"; +// } +// } + + + +TEST_CASE("parallel_map_performance_singlethread") +{ + constexpr auto size = problem_size; + ThreadPool pool(1); + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("parallel_map_singlethreaded", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + map(pool, v, std::move(vout), non_trivial_function) + ); + }); +} + +TEST_CASE("parallel_map_performance_2_threads") +{ + constexpr auto size = problem_size; + ThreadPool pool(2); + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("parallel_map_2_threads", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + map(pool, v, std::move(vout), non_trivial_function) + ); + }); +} + +TEST_CASE("parallel_map_performance_4_threads") +{ + constexpr auto size = problem_size; + ThreadPool pool(4); + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("parallel_map_4_threads", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + map(pool, v, std::move(vout), non_trivial_function) + ); + }); +} + +TEST_CASE("parallel_map_performance_8_threads") +{ + constexpr auto size = problem_size; + ThreadPool pool(8); + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("parallel_map_8_threads", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + map(pool, v, std::move(vout), non_trivial_function) + ); + }); +} + +TEST_CASE("transform_performance") +{ + constexpr auto size = problem_size; + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("std::transform", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + std::transform(v.cbegin(), v.cend(), vout.begin(), non_trivial_function) + ); + }); +} + +TEST_CASE("ranges::transform_performance") +{ + constexpr auto size = problem_size; + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + ankerl::nanobench::Bench().run("std::ranges::transform", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + std::ranges::transform(v.cbegin(), v.cend(), vout.begin(), non_trivial_function) + ); + }); +} + +TEST_CASE("for_loop_performance") +{ + constexpr auto size = problem_size; + std::vector v(size); + std::vector vout(size); + std::iota(v.begin(), v.end(), 0); + auto for_loop = [&]() + { + for (auto i = 0; i < vout.size(); ++i) + { + vout[i] = non_trivial_function(i); + } + return 0; + }; + + ankerl::nanobench::Bench().run("for_loop", [&]() + { + ankerl::nanobench::doNotOptimizeAway( + for_loop() + ); + }); +} + +auto static_func(int in) -> double { return static_cast(in); }; + +struct IncompatibleStruct {}; + +template +auto identity(Ty in) -> Ty { return in;} + +TEST_CASE("parallel_adapter_callability") +{ + ThreadPool pool(1); + std::vector in(0); + std::vector out(0); + const auto& in_ref = in; + auto& out_ref = out; + auto in_rref = std::move(in); + auto out_rref = std::move(out); + auto lambda = [&](auto x) { return x; }; + // auto r0 = map(pool, in, lambda); + // auto r1 = map(pool, in_ref, lambda); + // auto r2 = map(pool, in_rref, lambda); + // auto r3 = map(pool, std::move(in), lambda); // pointless but allowed + auto& r4 = map(pool, in, out_ref, lambda); // return is the same object as out_ref + auto& r5 = map(pool, in_ref, out_ref, lambda); // return is the same object as out_ref + auto& r6 = map(pool, in_rref, out_ref, lambda);// return is the same object as out_ref + + auto r7 = map(pool, in, std::move(out), lambda); // preferred passing. out is moved to return + auto r8 = map(pool, in_ref, std::move(out), lambda); // moved into the return value + auto r9 = map(pool, in_rref, std::move(out), lambda); + auto& r10 = map(pool, in, out_rref, lambda); + auto& r11 = map(pool, in_ref, out_rref, lambda); + auto& r12 = map(pool, in_rref, out_rref, lambda); + + auto& lambda_ref = lambda; + auto&& lambda_rref = lambda; + auto r13 = map(pool, in, out_rref, lambda_ref); + + std::function lambda_wrap = lambda; + auto& lambda_wrap_ref = lambda_wrap; + auto&& lambda_wrap_rref = std::move(lambda); + auto r14 = map(pool, in, out_rref, lambda_wrap); + auto r15 = map(pool, in, out_rref, lambda_wrap_ref); + auto r16 = map(pool, in, out_rref, lambda_wrap_rref); + auto r17 = map(pool, in, out_rref, static_func); + + // auto r18 = map(pool, in_ref, identity); +} + +TEST_CASE("parallel_map_correctness") +{ + ThreadPool pool(3); + const auto v = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto times_two = [](auto x){ return x * 2; }; + std::vector out; + + auto& out2 = map(pool, v, out, times_two); + CHECK(&out2 == &out); + for (auto id = 0; id < v.size(); ++id) { + CHECK(out.at(id) == 2 * id); + } +} + +TEST_CASE("parallel_enumerate_map_correctness") +{ + ThreadPool pool(3); + const auto v = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto times_two = [](auto idx, auto x){ return idx * x; }; + std::vector out; + + auto& out2 = enumerate_map(pool, v, out, times_two); + CHECK(&out2 == &out); + for (auto id = 0; id < v.size(); ++id) { + CHECK(out.at(id) == id * id); + } +} + +TEST_CASE("parallel_filter_correctness") +{ + ThreadPool pool(3); + const auto v = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto is_even = [](auto x){ return x % 2 == 0; }; + std::vector out; + + auto& out2 = filter(pool, v, out, is_even); + CHECK(&out2 == &out); + CHECK(out.size() == 5); + for (auto id = 0; id < out.size(); ++id) { + CHECK(out.at(id) == id * 2); + } +} + +TEST_CASE("parallel_map_filter_correctness") +{ + // TODO: There might be a race condition that causes a deadlock here + ThreadPool pool(3); + const auto v = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto times_two_is_even = [](int x) -> std::optional { + if (x % 2 == 0) { return std::make_optional(x * 2); } + else { return std::nullopt; } + }; + std::vector out; + + auto& out2 = Parallel::map_filter(pool, v, out, times_two_is_even); + CHECK(&out2 == &out); + CHECK(out.size() == 5); + for (auto id = 0; id < out.size(); ++id) { + CHECK(out.at(id) == id * 4); + } +} + +TEST_CASE("parallel_enumerate_map_filter_correctness") +{ + ThreadPool pool(3); + const auto v = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto times_two_is_even = [](size_t idx, int x) -> std::optional { + if (x % 2 == 0) { return std::make_optional(x * 2 + idx); } + else { return std::nullopt; } + }; + std::vector out; + + auto& out2 = Parallel::enumerate_map_filter(pool, v, out, times_two_is_even); + CHECK(&out2 == &out); + CHECK(out.size() == 5); + for (auto id = 0; id < out.size(); ++id) { + CHECK(out.at(id) == id * 6); + } +} + +TEST_CASE("parallel_enumerate_map2_filter2_correctness") +{ + ThreadPool pool(3); + const auto v1 = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto v2 = [] { + std::vector v(10); + std::iota(v.begin(), v.end(), 0); + return v; + }(); + const auto times_two_is_even = [](size_t idx, int x, int y) -> std::optional> { + if (x % 2 == 0) { return std::make_optional(std::make_pair(x, y)); } + else { return std::nullopt; } + }; + std::vector out1; + std::vector out2; + auto& out1_ref = out1; + auto& out2_ref = out2; + + // TODO: map_filter should check if the passed function actually returns an optional + Parallel::enumerate_map2_filter2(pool, v1, v2, out1, out2, times_two_is_even); + //CHECK(&out2 == &out); + CHECK(out1.size() == 5); + CHECK(out2.size() == 5); + + for (auto id = 0; id < out1.size(); ++id) { + CHECK(out1.at(id) == id * 2); + } + for (auto id = 0; id < out2.size(); ++id) { + CHECK(out2.at(id) == id * 2); + } +} \ No newline at end of file