Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/boundary_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const Eigen::MatrixXI> &F)
{
Eigen::VectorXI longest;
Expand Down
45 changes: 45 additions & 0 deletions src/resolve_duplicated_faces.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "default_types.h"
#include <igl/resolve_duplicated_faces.h>
#include <nanobind/nanobind.h>
#include <nanobind/eigen/dense.h>
#include <nanobind/stl/tuple.h>
#include <tuple>

namespace nb = nanobind;
using namespace nb::literals;

namespace pyigl
{
auto resolve_duplicated_faces(
const nb::DRef<const Eigen::MatrixXI> &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)");
}
8 changes: 4 additions & 4 deletions src/vertex_components.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
}
37 changes: 37 additions & 0 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Loading