diff --git a/README.md b/README.md index e1db2a3..78e587e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,42 @@ scale = 1000.0 quad[:,1:4,:] *= scale # Avoid scaling normals ``` +### Converting Triangles --> Vertices and Faces +```python +import openstl + +# Define an array of triangles +triangles = [ + # normal, vertices 0, vertices 1, vertices 2 + [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]], # Triangle 1 + [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]], # Triangle 2 +] + +# Convert triangles to vertices and faces +vertices, faces = openstl.convert.verticesandfaces(triangles) +``` + +### Converting Vertices and Faces --> Triangles +```python +import openstl + +# Define vertices and faces +vertices = [ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [2.0, 2.0, 2.0], + [3.0, 3.0, 3.0], +] + +faces = [ + [0, 1, 2], # Face 1 + [1, 3, 2] # Face 2 +] + +# Convert vertices and faces to triangles +triangles = openstl.convert.triangles(vertices, faces) +``` + # C++ Usage ### Read STL from file ```c++ diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index e6cc7da..4db2b23 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -25,5 +25,4 @@ configure_file(include/openstl/core/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/gen #------------------------------------------------------------------------------- add_library(openstl_core INTERFACE) target_include_directories(openstl_core INTERFACE include/ ${CMAKE_CURRENT_BINARY_DIR}/generated/) -target_link_libraries(openstl_core INTERFACE) add_library(openstl::core ALIAS openstl_core) \ No newline at end of file diff --git a/modules/core/include/openstl/core/stl.h b/modules/core/include/openstl/core/stl.h index e37ca0c..d69e574 100644 --- a/modules/core/include/openstl/core/stl.h +++ b/modules/core/include/openstl/core/stl.h @@ -24,18 +24,15 @@ SOFTWARE. #ifndef OPENSTL_OPENSTL_SERIALIZE_H #define OPENSTL_OPENSTL_SERIALIZE_H -#include #include #include -#include +#include #include -#include #include - -#include -#include +#include #include -#include +#include +#include namespace openstl { @@ -241,6 +238,8 @@ namespace openstl //--------------------------------------------------------------------------------------------------------- // Transformation Utils //--------------------------------------------------------------------------------------------------------- + using Face = std::array; // v0, v1, v2 + inline bool operator==(const Vec3& rhs, const Vec3& lhs) { return std::tie(rhs.x, rhs.y, rhs.z) == std::tie(lhs.x, lhs.y, lhs.z); } @@ -253,21 +252,94 @@ namespace openstl }; /** - * @brief Finds unique vertices from a vector of triangles. - * - * @param triangles The vector of triangles from which to find unique vertices. - * @return An unordered_set containing unique Vec3 vertices. + * @brief Find the inverse map: vertex -> face idx + * @param triangles The container of triangles from which to find unique vertices + * @return A hash map that maps: for each unique vertex -> a vector of corresponding face indices */ - inline std::unordered_set findUniqueVertices(const std::vector& triangles) { - std::unordered_set uniqueVertices; + template + inline std::unordered_map, Vec3Hash> findInverseMap(const Container& triangles) + { + std::unordered_map, Vec3Hash> map{}; + size_t triangleIdx{0}; + for (const auto& tri : triangles) { + for(const auto vertex : {&tri.v0, &tri.v1, &tri.v2}) + { + auto it = map.find(*vertex); + if (it != std::end(map)) { + it->second.emplace_back(triangleIdx); + continue; + } + map[*vertex] = {triangleIdx}; + } + ++triangleIdx; + } + return map; + } - for (const auto& triangle : triangles) { - uniqueVertices.insert({triangle.v0.x, triangle.v0.y, triangle.v0.z}); - uniqueVertices.insert({triangle.v1.x, triangle.v1.y, triangle.v1.z}); - uniqueVertices.insert({triangle.v2.x, triangle.v2.y, triangle.v2.z}); + + /** + * @brief Finds unique vertices from a vector of triangles + * @param triangles The container of triangles to convert + * @return An tuple containing respectively the vector of vertices and the vector of face indices + */ + template + inline std::tuple, std::vector> + convertToVerticesAndFaces(const Container& triangles) { + const auto& inverseMap = findInverseMap(triangles); + auto verticesNum = inverseMap.size(); + std::vector vertices{}; vertices.reserve(verticesNum); + std::vector faces(triangles.size()); + std::vector vertexPositionInFace(triangles.size(), 0u); + size_t vertexIdx{0}; + for(const auto& item : inverseMap) { + vertices.emplace_back(item.first); + // Multiple faces can have the same vertex index + for(const auto faceIdx : item.second) + faces[faceIdx][vertexPositionInFace[faceIdx]++] = vertexIdx; + ++vertexIdx; } + return std::make_tuple(std::move(vertices), std::move(faces)); + } + + inline Vec3 operator-(const Vec3& rhs, const Vec3& lhs) { + return {rhs.x - lhs.x, rhs.y - lhs.y, rhs.z - lhs.z}; + } - return uniqueVertices; + inline Vec3 crossProduct(const Vec3& a, const Vec3& b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; + } + + /** + * @brief Convert vertices and faces to triangles. + * @param vertices The container of vertices. + * @param faces The container of faces. + * @return A vector of triangles constructed from the vertices and faces. + */ + template + inline std::vector convertToTriangles(const ContainerA& vertices, const ContainerB& faces) + { + if (faces.size() == 0) + return {}; + + std::vector triangles; triangles.reserve(faces.size()); + auto getVertex = [&vertices](std::size_t index) { + return std::next(std::begin(vertices), index); + }; + auto minmax = std::max_element(&std::begin(faces)->at(0), &std::begin(faces)->at(0)+faces.size()*3); + + // Check if the minimum and maximum indices are within the bounds of the vector + if (*minmax >= static_cast(vertices.size())) { + throw std::out_of_range("Face index out of range"); + } + + for (const auto& face : faces) { + auto v0 = getVertex(face[0]); + auto v1 = getVertex(face[1]); + auto v2 = getVertex(face[2]); + const auto normal = crossProduct(*v1 - *v0, *v2 - *v0); + triangles.emplace_back(Triangle{normal, *v0, *v1, *v2, 0u}); + } + return triangles; } } //namespace openstl #endif //OPENSTL_OPENSTL_SERIALIZE_H diff --git a/python/core/src/stl.cpp b/python/core/src/stl.cpp index 95e5a43..5fc59a6 100644 --- a/python/core/src/stl.cpp +++ b/python/core/src/stl.cpp @@ -13,28 +13,34 @@ namespace py = pybind11; using namespace pybind11::literals; using namespace openstl; - -// Custom stride container for py::array_t -> Iterator of Triangles -class StridedTriangleSpan { +/** + * @brief Template class for creating a strided span over a contiguous sequence of memory. + * + * This class provides a strided view over a contiguous sequence of memory, allowing iteration + * over elements with a specified stride. + * + * @tparam VALUETYPE The type of elements stored in the span. + * @tparam SIZE The stride size (number of elements to skip between each element). + * @tparam PTRTYPE The type of the pointer to the underlying data. + */ +template +class StridedSpan { // Iterator type for iterating over elements with stride class Iterator { public: - // Define iterator category using iterator_category = std::forward_iterator_tag; using difference_type = std::ptrdiff_t; - using value_type = Triangle; - using pointer = Triangle*; - using reference = Triangle&; + using value_type = VALUETYPE; + using pointer = VALUETYPE*; + using reference = VALUETYPE&; - explicit Iterator(const float* ptr) : ptr(ptr) {} + explicit Iterator(const PTRTYPE* ptr) : ptr(ptr) {} - // Dereference operator - const Triangle& operator*() const { return *reinterpret_cast(ptr); } - const Triangle* operator->() const { return reinterpret_cast(ptr); } + const VALUETYPE& operator*() const { return *reinterpret_cast(ptr); } + const VALUETYPE* operator->() const { return reinterpret_cast(ptr); } - // Increment operator Iterator& operator++() { - ptr += 12; + ptr += SIZE; return *this; } @@ -45,23 +51,18 @@ class StridedTriangleSpan { bool operator!=(const Iterator& other) const { return !(*this == other); } private: - const float* ptr; + const PTRTYPE* ptr; }; - const float* data_; + const PTRTYPE* data_; size_t size_; public: + StridedSpan(const PTRTYPE* data, size_t size) : data_(data), size_(size) {} - // Constructor takes a pointer to the data, the number of elements, and the stride - StridedTriangleSpan(const float* data, size_t size) : data_(data), size_(size) {} - - // Begin iterator Iterator begin() const { return Iterator{data_}; } - - // End iterator - Iterator end() const { return Iterator{data_ + size_ * 12}; } - + Iterator end() const { return Iterator{data_ + size_ * SIZE}; } size_t size() const {return size_;} + const PTRTYPE* data() const {return data_;} }; @@ -83,7 +84,7 @@ namespace pybind11 { namespace detail { return false; std::vector triangles{}; triangles.reserve(buf.shape(0)); - StridedTriangleSpan stridedIter{buf.data(), (size_t)buf.shape(0)}; + StridedSpan stridedIter{buf.data(), (size_t)buf.shape(0)}; std::copy(std::begin(stridedIter), std::end(stridedIter), std::back_inserter(triangles)); @@ -92,7 +93,7 @@ namespace pybind11 { namespace detail { } static handle cast(const std::vector& src, return_value_policy /*policy*/, handle /* parent */) { - py::array_t array( + py::array_t array( {static_cast(src.size()), static_cast(4), static_cast(3)}, {sizeof(Triangle), sizeof(Vec3), sizeof(float)}, @@ -109,7 +110,7 @@ void serialize(py::module_ &m) { py::enum_(m, "format") .value("ascii", StlFormat::ASCII) .value("binary", StlFormat::Binary) - .export_values();; + .export_values(); m.def("write", [](const std::string &filename, const py::array_t &array, @@ -117,7 +118,7 @@ void serialize(py::module_ &m) { py::scoped_ostream_redirect stream(std::cerr,py::module_::import("sys").attr("stderr")); std::ofstream file(filename, std::ios::binary); if (!file.is_open()) { - std::cerr << "Error: Unable to open file '" << filename << "'" << std::endl; + std::cerr << "Error: Unable to open file '" << filename << "'." << std::endl; return false; } @@ -128,11 +129,11 @@ void serialize(py::module_ &m) { if (buf.ndim() != 3 || buf.shape(1) != 4 || buf.shape(2) != 3) return false; - StridedTriangleSpan stridedIter{buf.data(), (size_t)buf.shape(0)}; + StridedSpan stridedIter{buf.data(), (size_t)buf.shape(0)}; openstl::serialize(stridedIter, file, format); if (file.fail()) { - std::cerr << "Error: Failed to write to file " << filename << std::endl; + std::cerr << "Error: Failed to write to file '" << filename << "'." << std::endl; } file.close(); return true; @@ -142,7 +143,7 @@ void serialize(py::module_ &m) { py::scoped_ostream_redirect stream(std::cerr, py::module_::import("sys").attr("stderr")); std::ifstream file(filename, std::ios::binary); if (!file.is_open()) { - std::cerr << "Error: Unable to open file '" << filename << "'" << std::endl; + std::cerr << "Error: Unable to open file '" << filename << "'." << std::endl; return std::vector{}; } @@ -151,9 +152,87 @@ void serialize(py::module_ &m) { }, "filename"_a, "Deserialize a STl from a file", py::return_value_policy::move); } + +namespace openstl +{ + enum class Convert { VERTICES_AND_FACES=0, TRIANGLES}; +}; + +void convertSubmodule(py::module_ &_m) +{ + auto m = _m.def_submodule("convert", "A submodule to convert mesh representations"); + + m.def("verticesandfaces", []( + const py::array_t &array + ) + -> std::tuple, + py::array_t> + { + py::scoped_ostream_redirect stream(std::cerr,py::module_::import("sys").attr("stderr")); + auto buf = py::array_t::ensure(array); + if(!buf){ + std::cerr << "Input array cannot be interpreted as a mesh.\n"; + return {}; + } + if (buf.ndim() != 3 || buf.shape(1) != 4 || buf.shape(2) != 3){ + std::cerr << "Input array cannot be interpreted as a mesh.\n"; + return {}; + } + + StridedSpan stridedIter{buf.data(), (size_t)buf.shape(0)}; + const auto& verticesAndFaces = convertToVerticesAndFaces(stridedIter); + const auto& vertices = std::get<0>(verticesAndFaces); + const auto& faces = std::get<1>(verticesAndFaces); + + return std::make_tuple( + py::array_t( + {static_cast(vertices.size()),static_cast(3)}, + {sizeof(Vec3), sizeof(float)}, + (const float*)vertices.data()), + py::array_t( + {static_cast(faces.size()),static_cast(3)}, + {sizeof(Face), sizeof(size_t)}, + (const size_t*)faces.data()) + ); + }, "triangles"_a, "Convert the mesh to a format 'vertices-and-face-indices'"); + + + m.def("triangles", []( + const py::array_t &vertices, + const py::array_t &faces + ) -> std::vector + { + py::scoped_ostream_redirect stream(std::cerr,py::module_::import("sys").attr("stderr")); + auto vbuf = py::array_t::ensure(vertices); + if(!vbuf){ + std::cerr << "Vertices input array cannot be interpreted as a mesh.\n"; + return {}; + } + if (vbuf.ndim() != 2 || vbuf.shape(1) != 3){ + std::cerr << "Vertices input array cannot be interpreted as a mesh. Shape must be N x 3.\n"; + return {}; + } + + auto fbuf = py::array_t::ensure(faces); + if(!fbuf){ + std::cerr << "Faces input array cannot be interpreted as a mesh.\n"; + return {}; + } + if (fbuf.ndim() != 2 || vbuf.shape(1) != 3){ + std::cerr << "Faces input array cannot be interpreted as a mesh.\n"; + std::cerr << "Shape must be N x 3 (v0, v1, v2).\n"; + return {}; + } + + StridedSpan verticesIter{vbuf.data(), (size_t)vbuf.shape(0)}; + StridedSpan facesIter{fbuf.data(), (size_t)fbuf.shape(0)}; + return convertToTriangles(verticesIter, facesIter); + }, "vertices"_a,"faces"_a, "Convert the mesh from vertices and faces to triangles"); +} + PYBIND11_MODULE(openstl, m) { - //stl(m); serialize(m); + convertSubmodule(m); m.attr("__version__") = OPENSTL_PROJECT_VER; m.doc() = "A simple STL serializer and deserializer"; diff --git a/tests/core/src/utilities.test.cpp b/tests/core/src/utilities.test.cpp index a7a5aea..434402f 100644 --- a/tests/core/src/utilities.test.cpp +++ b/tests/core/src/utilities.test.cpp @@ -1,25 +1,234 @@ #include -#include "openstl/tests/testutils.h" #include "openstl/core/stl.h" +#include +#include using namespace openstl; -TEST_CASE("Find unique triangles", "[openstl]") { - Vec3 v0{1.0f, 2.0f, 3.0f}, v1{4.0f, 5.0f, 6.0f}, v2{7.0f, 8.0f, 9.0f}, v3{10.0f, 20.0f, 30.0f}; - Vec3 normal{0.f, 0.f, 1.f}; - std::vector triangles = { - {normal, v0, v1, v2, 0}, - {normal, v0, v1, v2, 0}, // Duplicate vertices - {normal, v3, v3, v3, 0}, + + +TEST_CASE("findInverseMap function test", "[openstl::core]") { + SECTION("Empty input vector") { + std::vector triangles; + auto inverseMap = findInverseMap(triangles); + REQUIRE(inverseMap.empty()); + } + + SECTION("Input vector with one triangle") { + std::vector triangles = { + Triangle{{1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0} + }; + auto inverseMap = findInverseMap(triangles); + REQUIRE(inverseMap.size() == 3); + REQUIRE(inverseMap[triangles[0].v0].size() == 1); + REQUIRE(inverseMap[triangles[0].v1].size() == 1); + REQUIRE(inverseMap[triangles[0].v2].size() == 1); + } + + SECTION("Input vector with multiple triangles") { + std::vector triangles = { + Triangle{{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0}, + Triangle{{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0}, + Triangle{{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0} + }; + auto inverseMap = findInverseMap(triangles); + REQUIRE(inverseMap.size() == 3); + REQUIRE(inverseMap[triangles[0].v0].size() == 3); // All vertices are similar + REQUIRE(inverseMap[triangles[0].v1].size() == 3); + REQUIRE(inverseMap[triangles[0].v2].size() == 3); + } + SECTION("Multiples vertices and different triangles") + { + const Vec3 v0{1.0f, 2.0f, 3.0f}, v1{4.0f, 5.0f, 6.0f}, v2{7.0f, 8.0f, 9.0f}, v3{10.0f, 20.0f, 30.0f}; + const Vec3 normal{0.f, 0.f, 1.f}; + std::vector triangles = { + {normal, v0, v1, v2, 0}, + {normal, v2, v1, v0, 0}, // Duplicate vertices + {normal, v0, v1, v3, 0}, + }; + const auto& inverseMap = findInverseMap(triangles); + REQUIRE(inverseMap.size() == 4); + + // Check if specific vertices are present in the set + REQUIRE(inverseMap.count(v0) == 1); + REQUIRE(inverseMap.count(v1) == 1); + REQUIRE(inverseMap.count(v2) == 1); + REQUIRE(inverseMap.count(v3) == 1); + REQUIRE(inverseMap.count(normal) == 0); // Not a vertex, a normal + + // Check the number of face indices per vertex + REQUIRE(inverseMap.at(v0).size() == 3); + REQUIRE(inverseMap.at(v1).size() == 3); + REQUIRE(inverseMap.at(v2).size() == 2); + REQUIRE(inverseMap.at(v3).size() == 1); + } +} + +TEST_CASE("convertToVerticesAndFaces function test", "[convertToVerticesAndFaces]") { + SECTION("Empty input vector") { + std::vector triangles; + auto result = convertToVerticesAndFaces(triangles); + REQUIRE(std::get<0>(result).empty()); + REQUIRE(std::get<1>(result).empty()); + } + + SECTION("Input vector with one triangle") { + std::vector triangles = { + Triangle{{1.0f, 2.0f, 3.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0} + }; + auto result = convertToVerticesAndFaces(triangles); + REQUIRE(std::get<0>(result).size() == 3); + REQUIRE(std::get<1>(result).size() == 1); + } + SECTION("Input vector with multiple triangles") { + std::vector triangles = { + Triangle{{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0}, + Triangle{{0.0f, 0.0f, 2.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, 0}, + Triangle{{0.0f, 0.0f, 3.0f}, {1.0f, 1.0f, 0.0f}, {2.0f, 1.0f, 0.0f}, {1.0f, 2.0f, 0.0f}, 0} + }; + auto result = convertToVerticesAndFaces(triangles); + const auto& vertices = std::get<0>(result); + const auto& faces = std::get<1>(result); + + REQUIRE(vertices.size() == 6); // There are 9 unique vertices in the triangles + REQUIRE(faces.size() == 3); // There are 3 triangles + + // Check if each face has three vertices + for (const auto& face : faces) { + REQUIRE(face.size() == 3); // v0, v1, v2 + // Check if all indices in each face are unique + std::unordered_set uniqueIndices(std::begin(face), std::end(face)); + REQUIRE(uniqueIndices.size() == face.size()); + } + + // Check the correctness of vertices and faces + // Here we are just checking if the vertices and faces are correctly extracted + std::unordered_set uniqueVertices(std::begin(vertices), std::end(vertices)); + REQUIRE(uniqueVertices.size() == vertices.size()); // Check for uniqueness of vertices + + // Check if each face contains valid indices to the vertices + for (const auto& face : faces) { + for (size_t vertexIdx : face) { + REQUIRE(vertexIdx >= 0); + REQUIRE(vertexIdx < vertices.size()); + } + } + } +} + +TEST_CASE("convertToTriangles function test", "[convertToTriangles]") { + SECTION("Face index out of range") { + std::vector vertices = { + {0.0f, 0.0f, 1.0f} + }; + std::vector faces = { + {0, 1, 2} + }; + REQUIRE_THROWS_AS(convertToTriangles(vertices, faces), std::out_of_range); + } + SECTION("Valid input") { + Vec3 v0{0.0f, 0.0f, 0.0f}; + Vec3 v1{1.0f, 0.0f, 0.0f}; + Vec3 v2{0.0f, 1.0f, 0.0f}; + std::vector vertices = { + v0,v1,v2, + {0.0f, 5.0f, 0.0f} // extra vertex, not indexed + }; + + std::vector faces = { + {0, 1, 2} // v0,v1,v2 + }; + + auto triangles = convertToTriangles(vertices, faces); + + REQUIRE(triangles.size() == 1); + + const auto& triangle = triangles[0]; + REQUIRE(triangle.v0 == v0); + REQUIRE(triangle.v1 == v1); + REQUIRE(triangle.v2 == v2); + } +} + +template +bool areAllUnique(const std::array& arr) { + std::unordered_set seen; + for (const auto& element : arr) { + if (!seen.insert(element).second) { + // If insertion fails, the element is not unique + return false; + } + } + // If the loop completes, all elements are unique + return true; +} + +// Helper function to check if two Vec3 objects are equal +bool areVec3Equal(const Vec3& v1, const Vec3& v2) { + return v1.x == v2.x && v1.y == v2.y && v1.z == v2.z; +} + +// Helper function to check if two Face objects are equal. +// Vertices v0, v1, v2 can be shuffled between two equal faces +bool areFacesEqual(const Face& f1, const Face& f2, const std::vector &v1, const std::vector &v2) { + assert(areAllUnique(f1) && areAllUnique(f2)); + return std::all_of( + std::begin(f1),std::end(f1), + [&](const size_t &idx_f1) { + return std::any_of( + std::begin(f2), std::end(f2), + [&](const size_t &idx_f2) { return areVec3Equal(v1[idx_f1], v2[idx_f2]); } + ); + } + ); +} + +TEST_CASE("convertToVerticesAndFaces <-> convertToTriangles integration test", "[integration]") { + std::vector vertices = { + {0.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {1.0f, 1.0f, 0.0f}, + {0.5f, 0.5f, 1.0f}, + }; + + // Convert vertices to faces + std::vector faces = { + {0, 1, 2}, // indices for: v0, v1, v2 + {1, 3, 2}, + {2, 3, 4}, }; - auto uniqueVertices = findUniqueVertices(triangles); - REQUIRE(uniqueVertices.size() == 4); + // Convert faces to triangles + auto triangles = convertToTriangles(vertices, faces); + + // Convert triangles back to vertices and faces + auto result = convertToVerticesAndFaces(triangles); + const auto& finalVertices = std::get<0>(result); + const auto& finalFaces = std::get<1>(result); + + // Check if all original vertices are present in the final vertices + bool allVerticesFound = std::all_of( + std::begin(vertices),std::end(vertices), + [&finalVertices](const Vec3 &vertex) { + return std::any_of( + std::begin(finalVertices), std::end(finalVertices), + [&vertex](const Vec3 &final_v) { return areVec3Equal(vertex, final_v); } + ); + } + ); + REQUIRE(allVerticesFound); + - // Check if specific vertices are present in the set - REQUIRE(uniqueVertices.count(v0) == 1); - REQUIRE(uniqueVertices.count(v1) == 1); - REQUIRE(uniqueVertices.count(v2) == 1); - REQUIRE(uniqueVertices.count(v3) == 1); - REQUIRE(uniqueVertices.count(normal) == 0); // Not a vertice, a normal + // Check if all original faces are present in the final faces + bool allFacesValid = std::all_of( + std::begin(faces),std::end(faces), + [&](const Face &face) { + return std::any_of( + std::begin(finalFaces), std::end(finalFaces), + [&](const Face &final_f) { return areFacesEqual(face, final_f, vertices, finalVertices); } + ); + } + ); + REQUIRE(allFacesValid); } \ No newline at end of file diff --git a/tests/python/test_stl.py b/tests/python/test_stl.py index b18423e..bb86cff 100644 --- a/tests/python/test_stl.py +++ b/tests/python/test_stl.py @@ -38,5 +38,108 @@ def test_fail_on_read(): assert len(triangles_read) == 0 +# Define Face and Vec3 as tuples +Face = tuple[int, int, int] # v0, v1, v2 +Vec3 = tuple[float, float, float] + +def are_all_unique(arr: list) -> bool: + """Check if all elements in the array are unique.""" + seen = set() + for element in arr: + if element in seen: + return False + seen.add(element) + return True + + +def are_faces_equal(face1: Face, face2: Face, v1: list[Vec3], v2: list[Vec3]) -> bool: + """Check if two Face objects are equal.""" + # Vertices v0, v1, v2 can be shuffled between two equal faces + assert len(np.unique(face1)) == len(np.unique(face2)) + for i in face1: + if not any((v1[i] == v2[j]).all() for j in face2): + return False + return True + + +def all_faces_valid(faces: list[Face], final_faces: list[Face], vertices: list[Vec3], final_vertices: list[Vec3]) -> bool: + """Check if all original faces are present in the final faces.""" + return all(any(are_faces_equal(face, final_f, vertices, final_vertices) for final_f in final_faces) for face in faces) + + +@pytest.fixture +def sample_vertices_and_faces(): + # Define vertices and faces + vertices = np.array([ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], + [2.0, 2.0, 2.0], + [3.0, 3.0, 3.0], + ]) + faces = np.array([ + [0, 1, 2], # Face 1 + [1, 3, 2] # Face 2 + ]) + return vertices, faces + + +def test_convert_to_vertices_and_faces_on_empty(): + empty_triangles = np.array([[]]) + vertices, faces = openstl.convert.verticesandfaces(empty_triangles) + # Test if vertices and faces are empty + assert len(vertices) == 0 + assert len(faces) == 0 + +def test_convert_to_vertices_and_faces(sample_triangles): + vertices, faces = openstl.convert.verticesandfaces(sample_triangles) + # Convert vertices to tuples to make them hashable + vertices = [tuple(vertex) for vertex in vertices] + + # Test if vertices and faces are extracted correctly + assert len(vertices) == 3 + assert len(faces) == 1000 + + # Test if each face contains three indices + for face in faces: + assert len(face) == 3 + + # Test for uniqueness of vertices + unique_vertices = set(vertices) + assert len(unique_vertices) == len(vertices) + + # Test if all indices in faces are valid + for face in faces: + for vertex_idx in face: + assert vertex_idx >= 0 + assert vertex_idx < len(vertices) + + +def test_convertToVerticesAndFaces_integration(sample_vertices_and_faces): + # Extract vertices and faces + vertices, faces = sample_vertices_and_faces + + # Convert vertices and faces to triangles + triangles = openstl.convert.triangles(vertices, faces) + + # Convert triangles back to vertices and faces + result_vertices, result_faces = openstl.convert.verticesandfaces(triangles) + + # Check if the number of vertices and faces are preserved + assert len(vertices) == len(result_vertices) + assert len(faces) == len(result_faces) + + # Check if each vertices are preserved. + found_set: list[int] = [] + for i, result_vertex in enumerate(result_vertices): + for ref_vertex in vertices: + if (ref_vertex == result_vertex).all(): + found_set.append(i) + break + assert len(found_set) == result_vertices.shape[0] + + # Check if each face is correctly preserved + for face, result_face in zip(faces, result_faces): + assert are_faces_equal(face, result_face, vertices, result_vertices) + if __name__ == "__main__": pytest.main() \ No newline at end of file