diff --git a/include/OpenABF/ABF.hpp b/include/OpenABF/ABF.hpp index b8a02d8..7dafc56 100644 --- a/include/OpenABF/ABF.hpp +++ b/include/OpenABF/ABF.hpp @@ -395,11 +395,11 @@ class ABF for (auto& f : mesh->faces()) { f->lambda_tri += delta(idx++, 0); } + auto base = edgeCnt + faceCnt; for (auto& v : mesh->vertices_interior()) { auto intIdx = vIdx2vIntIdx.at(v->idx); - v->lambda_plan += delta(idx + intIdx, 0); - v->lambda_len += delta(idx + vIntCnt + intIdx, 0); - idx++; + v->lambda_plan += delta(base + intIdx, 0); + v->lambda_len += delta(base + vIntCnt + intIdx, 0); } // Recalculate gradient for next iteration diff --git a/include/OpenABF/Vec.hpp b/include/OpenABF/Vec.hpp index 0d59cb8..6f269df 100644 --- a/include/OpenABF/Vec.hpp +++ b/include/OpenABF/Vec.hpp @@ -107,24 +107,24 @@ class Vec constexpr const_iterator cend() const noexcept { return val_.cend(); } /** @brief Get an iterator to the first element of the reverse vector */ - constexpr iterator rbegin() noexcept { return val_.rbegin(); } - /** @brief Get an iterator to the first element of the vector */ - constexpr const_iterator rbegin() const noexcept { return val_.rbegin(); } - /** @brief Get an iterator to the first element of the vector */ - constexpr const_iterator crbegin() const noexcept { return val_.crbegin(); } + constexpr reverse_iterator rbegin() noexcept { return val_.rbegin(); } + /** @brief Get an iterator to the first element of the reverse vector */ + constexpr const_reverse_iterator rbegin() const noexcept { return val_.rbegin(); } + /** @brief Get an iterator to the first element of the reverse vector */ + constexpr const_reverse_iterator crbegin() const noexcept { return val_.crbegin(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr iterator rend() noexcept { return val_.rend(); } + constexpr reverse_iterator rend() noexcept { return val_.rend(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr const_iterator rend() const noexcept { return val_.rend(); } + constexpr const_reverse_iterator rend() const noexcept { return val_.rend(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr const_iterator crend() const noexcept { return val_.crend(); } + constexpr const_reverse_iterator crend() const noexcept { return val_.crend(); } /** @brief Return whether the vector is empty (uninitialized) */ constexpr bool empty() const noexcept { return val_.empty(); } diff --git a/single_include/OpenABF/OpenABF.hpp b/single_include/OpenABF/OpenABF.hpp index 3fb199e..b2f323e 100644 --- a/single_include/OpenABF/OpenABF.hpp +++ b/single_include/OpenABF/OpenABF.hpp @@ -259,24 +259,24 @@ class Vec constexpr const_iterator cend() const noexcept { return val_.cend(); } /** @brief Get an iterator to the first element of the reverse vector */ - constexpr iterator rbegin() noexcept { return val_.rbegin(); } - /** @brief Get an iterator to the first element of the vector */ - constexpr const_iterator rbegin() const noexcept { return val_.rbegin(); } - /** @brief Get an iterator to the first element of the vector */ - constexpr const_iterator crbegin() const noexcept { return val_.crbegin(); } + constexpr reverse_iterator rbegin() noexcept { return val_.rbegin(); } + /** @brief Get an iterator to the first element of the reverse vector */ + constexpr const_reverse_iterator rbegin() const noexcept { return val_.rbegin(); } + /** @brief Get an iterator to the first element of the reverse vector */ + constexpr const_reverse_iterator crbegin() const noexcept { return val_.crbegin(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr iterator rend() noexcept { return val_.rend(); } + constexpr reverse_iterator rend() noexcept { return val_.rend(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr const_iterator rend() const noexcept { return val_.rend(); } + constexpr const_reverse_iterator rend() const noexcept { return val_.rend(); } /** * @brief Get an iterator to one past the last element in the reverse vector */ - constexpr const_iterator crend() const noexcept { return val_.crend(); } + constexpr const_reverse_iterator crend() const noexcept { return val_.crend(); } /** @brief Return whether the vector is empty (uninitialized) */ constexpr bool empty() const noexcept { return val_.empty(); } @@ -2301,11 +2301,11 @@ class ABF for (auto& f : mesh->faces()) { f->lambda_tri += delta(idx++, 0); } + auto base = edgeCnt + faceCnt; for (auto& v : mesh->vertices_interior()) { auto intIdx = vIdx2vIntIdx.at(v->idx); - v->lambda_plan += delta(idx + intIdx, 0); - v->lambda_len += delta(idx + vIntCnt + intIdx, 0); - idx++; + v->lambda_plan += delta(base + intIdx, 0); + v->lambda_len += delta(base + vIntCnt + intIdx, 0); } // Recalculate gradient for next iteration diff --git a/tests/src/TestHalfEdgeMesh.cpp b/tests/src/TestHalfEdgeMesh.cpp index 489248d..9989a8a 100644 --- a/tests/src/TestHalfEdgeMesh.cpp +++ b/tests/src/TestHalfEdgeMesh.cpp @@ -255,6 +255,26 @@ TEST(HalfEdgeMesh, FindPath) EXPECT_EQ(indices, expected); } +TEST(HalfEdgeMesh, FindPath_Disconnected) +{ + // Build two disconnected triangles (separate connected components) + const auto mesh = MeshType::New(); + // CC1: vertices 0-2 + mesh->insert_vertex(0, 0, 0); + mesh->insert_vertex(1, 0, 0); + mesh->insert_vertex(0, 1, 0); + mesh->insert_face(0, 2, 1); + // CC2: vertices 3-5 + mesh->insert_vertex(5, 0, 0); + mesh->insert_vertex(6, 0, 0); + mesh->insert_vertex(5, 1, 0); + mesh->insert_face(3, 5, 4); + + // Path between vertices in different CCs should be empty + const auto path = FindEdgePath(mesh, 0, 3); + EXPECT_TRUE(path.empty()); +} + TEST(HalfEdgeMesh, Clone) { const auto mesh = ConstructPyramid(); diff --git a/tests/src/TestMeshIO.cpp b/tests/src/TestMeshIO.cpp index 465d79b..15db80e 100644 --- a/tests/src/TestMeshIO.cpp +++ b/tests/src/TestMeshIO.cpp @@ -36,6 +36,46 @@ TEST(MeshIO, PLY_ReadWrite) EXPECT_EQ(mesh->num_faces(), mesh2->num_faces()); } +TEST(MeshIO, OBJ_RoundTrip) +{ + auto mesh = ConstructPyramid(); + + std::ostringstream oss; + io_formats::OBJ::Write(oss, *mesh); + + auto mesh2 = MeshType::New(); + std::istringstream iss(oss.str()); + io_formats::OBJ::Read(iss, *mesh2); + + ASSERT_EQ(mesh->num_vertices(), mesh2->num_vertices()); + ASSERT_EQ(mesh->num_faces(), mesh2->num_faces()); + for (std::size_t v = 0; v < mesh->num_vertices(); ++v) { + for (int i = 0; i < 3; ++i) { + EXPECT_FLOAT_EQ(mesh->vertex(v)->pos[i], mesh2->vertex(v)->pos[i]); + } + } +} + +TEST(MeshIO, PLY_RoundTrip) +{ + auto mesh = ConstructPyramid(); + + std::ostringstream oss; + io_formats::PLY::Write(oss, *mesh); + + auto mesh2 = MeshType::New(); + std::istringstream iss(oss.str()); + io_formats::PLY::Read(iss, *mesh2); + + ASSERT_EQ(mesh->num_vertices(), mesh2->num_vertices()); + ASSERT_EQ(mesh->num_faces(), mesh2->num_faces()); + for (std::size_t v = 0; v < mesh->num_vertices(); ++v) { + for (int i = 0; i < 3; ++i) { + EXPECT_FLOAT_EQ(mesh->vertex(v)->pos[i], mesh2->vertex(v)->pos[i]); + } + } +} + TEST(MeshIO, ReadWriteFile) { auto mesh = ConstructPyramid(); diff --git a/tests/src/TestParameterization.cpp b/tests/src/TestParameterization.cpp index 153c1f5..eae7c27 100644 --- a/tests/src/TestParameterization.cpp +++ b/tests/src/TestParameterization.cpp @@ -1,3 +1,4 @@ +#include #include #include "OpenABF/OpenABF.hpp" @@ -62,4 +63,75 @@ TEST(Parameterizations, ABFPlusPlus) EXPECT_FLOAT_EQ(vv->pos[i], ve[i]); } } +} + +TEST(Parameterizations, ABF_MultiInterior) +{ + // 4×4 vertex grid → 4 interior vertices; exercises the multi-interior code path + using ABFType = ABF; + using LSCM = AngleBasedLSCM; + + auto mesh = ConstructGrid(4, 4); + ASSERT_EQ(mesh->num_vertices_interior(), 4u); + + ABFType::Compute(mesh); + LSCM::Compute(mesh); + + // After LSCM, all UV z-coordinates must be zero and x/y must be finite + for (std::size_t v = 0; v < mesh->num_vertices(); ++v) { + const auto& pos = mesh->vertex(v)->pos; + EXPECT_TRUE(std::isfinite(pos[0])) << "vertex " << v << " x is not finite"; + EXPECT_TRUE(std::isfinite(pos[1])) << "vertex " << v << " y is not finite"; + EXPECT_FLOAT_EQ(pos[2], 0.f); + } +} + +TEST(Parameterizations, ABFPlusPlus_MultiInterior) +{ + // 4×4 vertex grid → 4 interior vertices + using ABFType = ABFPlusPlus; + using LSCM = AngleBasedLSCM; + + auto mesh = ConstructGrid(4, 4); + ASSERT_EQ(mesh->num_vertices_interior(), 4u); + + ABFType::Compute(mesh); + LSCM::Compute(mesh); + + for (std::size_t v = 0; v < mesh->num_vertices(); ++v) { + const auto& pos = mesh->vertex(v)->pos; + EXPECT_TRUE(std::isfinite(pos[0])) << "vertex " << v << " x is not finite"; + EXPECT_TRUE(std::isfinite(pos[1])) << "vertex " << v << " y is not finite"; + EXPECT_FLOAT_EQ(pos[2], 0.f); + } +} + +TEST(Parameterizations, ABF_NoInteriorVertices) +{ + // Single triangle: 0 interior vertices — solver must not crash + using ABFType = ABF; + using LSCM = AngleBasedLSCM; + + auto mesh = ABFType::Mesh::New(); + mesh->insert_vertex(0, 0, 0); + mesh->insert_vertex(1, 0, 0); + mesh->insert_vertex(0, 1, 0); + mesh->insert_face(0, 2, 1); + + ASSERT_EQ(mesh->num_vertices_interior(), 0u); + EXPECT_NO_THROW(ABFType::Compute(mesh)); + EXPECT_NO_THROW(LSCM::Compute(mesh)); +} + +TEST(Parameterizations, ABF_MaxIters) +{ + // Instance API: limit to 1 iteration and confirm the solver respects it + using ABFType = ABF; + + auto mesh = ConstructPyramid(); + ABFType abf; + abf.setMaxIterations(1); + abf.compute(mesh); + + EXPECT_EQ(abf.iterations(), 1u); } \ No newline at end of file diff --git a/tests/src/TestVec.cpp b/tests/src/TestVec.cpp index f588918..e44dff8 100644 --- a/tests/src/TestVec.cpp +++ b/tests/src/TestVec.cpp @@ -147,4 +147,24 @@ TEST(Vec, UnitVector) Vec3f a{2, 0, 0}; EXPECT_EQ(a.unit(), Vec3f(1, 0, 0)); EXPECT_EQ(a, Vec3f(2, 0, 0)); +} + +TEST(Vec, ReverseIteration) +{ + Vec3f v{1.f, 2.f, 3.f}; + + // rbegin/rend + std::vector result; + for (auto it = v.rbegin(); it != v.rend(); ++it) { + result.push_back(*it); + } + EXPECT_EQ(result, (std::vector{3.f, 2.f, 1.f})); + + // crbegin/crend on a const Vec3f + const Vec3f cv{1.f, 2.f, 3.f}; + std::vector cresult; + for (auto it = cv.crbegin(); it != cv.crend(); ++it) { + cresult.push_back(*it); + } + EXPECT_EQ(cresult, (std::vector{3.f, 2.f, 1.f})); } \ No newline at end of file diff --git a/tests/src/Utils.hpp b/tests/src/Utils.hpp index 3e7a078..694d6a9 100644 --- a/tests/src/Utils.hpp +++ b/tests/src/Utils.hpp @@ -17,4 +17,32 @@ auto ConstructPyramid() -> typename MeshType::Pointer mesh->insert_faces({{1, 3, 0}, {3, 2, 0}, {3, 1, 2}}); return mesh; } + +/** + * Construct a flat rectangular grid mesh, triangulated by splitting each quad + * along its diagonal. A (rows x cols) vertex grid produces (rows-1)*(cols-1)*2 + * triangles. Interior vertex count = (rows-2)*(cols-2). + * + * Example: ConstructGrid(4, 4) → 16 vertices, 18 faces, 4 interior vertices. + */ +template +auto ConstructGrid(std::size_t rows, std::size_t cols) -> typename MeshType::Pointer +{ + auto mesh = MeshType::New(); + for (std::size_t r = 0; r < rows; ++r) { + for (std::size_t c = 0; c < cols; ++c) { + mesh->insert_vertex(static_cast(c), static_cast(r), 0.f); + } + } + for (std::size_t r = 0; r < rows - 1; ++r) { + for (std::size_t c = 0; c < cols - 1; ++c) { + auto v0 = r * cols + c; + auto v1 = r * cols + c + 1; + auto v2 = (r + 1) * cols + c; + auto v3 = (r + 1) * cols + c + 1; + mesh->insert_faces({{v0, v2, v1}, {v1, v2, v3}}); + } + } + return mesh; +} } // namespace OpenABF::tests