diff --git a/src/boundary_loop.cpp b/src/boundary_loop.cpp index d08a854c..25873ed8 100644 --- a/src/boundary_loop.cpp +++ b/src/boundary_loop.cpp @@ -16,7 +16,7 @@ namespace pyigl igl::boundary_loop(F, loops); return loops; } - // Wrapper for boundary_loop that returns all loops as a vector of vectors + // Wrapper for boundary_loop that returns only the longest loop as a vector auto boundary_loop(const nb::DRef &F) { Eigen::VectorXI longest; diff --git a/src/resolve_duplicated_faces.cpp b/src/resolve_duplicated_faces.cpp new file mode 100644 index 00000000..4cca4179 --- /dev/null +++ b/src/resolve_duplicated_faces.cpp @@ -0,0 +1,45 @@ +#include "default_types.h" +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + auto resolve_duplicated_faces( + const nb::DRef &F1) + { + // resolve_duplicated_faces reuses the input's Derived type for internal + // plain objects, so it needs a concrete matrix rather than an Eigen::Ref. + const Eigen::MatrixXI F1c = F1; + Eigen::MatrixXI F2; + Eigen::VectorXI J; + igl::resolve_duplicated_faces(F1c, F2, J); + return std::make_tuple(F2, J); + } +} + +void bind_resolve_duplicated_faces(nb::module_ &m) +{ + m.def("resolve_duplicated_faces", &pyigl::resolve_duplicated_faces, "F1"_a, + R"(Resolve duplicated faces according to the following rules per unique face: + +1. If the number of positively oriented faces equals the number of negatively + oriented faces, remove all duplicated faces at this triangle. +2. If the number of positively oriented faces equals the number of negatively + oriented faces plus 1, keep one of the positively oriented faces. +3. If the number of positively oriented faces equals the number of negatively + oriented faces minus 1, keep one of the negatively oriented faces. +4. If the number of positively oriented faces differs from the number of + negatively oriented faces by more than 1, the mesh is not orientable at that + triangle and all of its copies are dropped. + +@param[in] F1 #F1 by 3 array of input faces +@return Tuple (F2, J) where + F2 #F2 by 3 array of output faces without duplicated faces + J #F2 list of indices into F1)"); +} diff --git a/src/vertex_components.cpp b/src/vertex_components.cpp index 40be01c5..850aae9b 100644 --- a/src/vertex_components.cpp +++ b/src/vertex_components.cpp @@ -21,13 +21,13 @@ namespace pyigl // Bind the wrappers to the Python module void bind_vertex_components(nb::module_ &m) { - // Binding for vertex_components with adjacency matrix and counts + // Binding for vertex_components from a mesh's face list m.def( "vertex_components", &pyigl::vertex_components, "F"_a, - R"(Compute the connected components of a graph using an adjacency matrix, returning component IDs and counts. + R"(Compute the connected component ids of each vertex of a mesh given its faces. -@param[in] F Matrix of triangle indices -@return Vector C of component IDs per vertex)"); +@param[in] F #F by 3 matrix of triangle (face) indices +@return Vector C of per-vertex connected-component ids)"); } diff --git a/tests/test_all.py b/tests/test_all.py index 96f7533a..370c7431 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1754,3 +1754,40 @@ def test_predicates_spline_winding_number(): assert W.shape == (2,) assert np.isclose(abs(W[0]), 1.0, atol=1e-9) assert np.isclose(W[1], 0.0, atol=1e-9) + + +# -------------------------------------------------------------------------- +# resolve_duplicated_faces +# -------------------------------------------------------------------------- + +def test_resolve_duplicated_faces(): + # A mesh with no duplicated faces is returned unchanged. + F = np.array([[0, 1, 2], [2, 3, 0]], dtype=np.int64) + F2, J = igl.resolve_duplicated_faces(F) + assert np.array_equal(F2, F) + assert list(J.ravel()) == [0, 1] + + # A pair of oppositely-oriented copies of the same face cancels out, while a + # distinct unique face is kept (rule 1). + F = np.array([[0, 1, 2], # unique face, kept + [2, 1, 3], # face B, + + [3, 1, 2]], # face B reversed, - -> cancels with the above + dtype=np.int64) + F2, J = igl.resolve_duplicated_faces(F) + assert set(map(tuple, F2.tolist())) == {(0, 1, 2)} + assert list(J.ravel()) == [0] + + # Two positive copies and one negative copy: keep a single positive face + # (rule 2). J indexes back into the input. + F = np.array([[0, 1, 2], [0, 1, 2], [1, 0, 2]], dtype=np.int64) + F2, J = igl.resolve_duplicated_faces(F) + assert F2.shape[0] == 1 + assert tuple(F2[0]) == (0, 1, 2) + assert F.shape[1] == F2.shape[1] + assert np.array_equal(F[J.ravel()], F2) + + # Non-orientable triangle (three identical copies, |pos-neg| > 1): all of its + # copies are dropped, unrelated faces survive (rule 4). + F = np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2], [3, 4, 5]], dtype=np.int64) + F2, J = igl.resolve_duplicated_faces(F) + assert set(map(tuple, F2.tolist())) == {(3, 4, 5)}