From 0db8941e0a276f7eebd3472886aed18cfa1f7250 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 10 Apr 2018 15:26:21 +0200 Subject: [PATCH 01/65] is_degenerate_edge function --- .../CGAL/Polygon_mesh_processing/repair.h | 48 +++++++++++++++++++ .../remove_degeneracies_test.cpp | 16 +++++++ 2 files changed, 64 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 6fcb1e929dd..02d71ec97a8 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -186,6 +186,54 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) return degenerate_faces(tm, get(vertex_point, tm), Kernel(), out); } +/// \ingroup PMP_repairing_grp +/// checks whether an edge is degenerate. +/// An edge is considered degenerate if two of its vertices share the same location. +/// +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param pm the triangulated surface mesh to be repaired +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor : +/// - `Equal_3` to check whether 2 points are identical +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the edge is degenerate +template +bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, + PolygonMesh& pm, + const NamedParameters& np) +{ + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_property_map(vertex_point, pm)); + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + if ( traits.equal_3_object()(get(vpmap, target(e, pm)), get(vpmap, source(e, pm))) ) + return true; +} + +template +bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, + PolygonMesh& pm) +{ + return is_degenerate_edge(e, pm, parameters::all_default()); +} + // this function remove a border edge even if it does not satisfy the link condition. // The only limitation is that the length connected component of the boundary this edge // is strictly greater than 3 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 386e27723a1..3e069bf79be 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -29,6 +30,20 @@ void fix(const char* fname) assert( CGAL::is_valid_polygon_mesh(mesh) ); } +void check_edge_degeneracy(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + BOOST_FOREACH(typename boost::graph_traits::edge_descriptor e, edges(mesh)) + CGAL::Polygon_mesh_processing::is_degenerate_edge(e, mesh); +} + int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -38,6 +53,7 @@ int main() fix("data_degeneracies/degtri_three.off"); fix("data_degeneracies/degtri_single.off"); fix("data_degeneracies/trihole.off"); + check_edge_degeneracy("data_degeneracies/degtri_edge.off"); return 0; } From 8e285cb1a778c22979a64d43b0bd0026c3b80e3b Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 10 Apr 2018 16:58:13 +0200 Subject: [PATCH 02/65] is_degenerate_triangle_face function --- .../CGAL/Polygon_mesh_processing/repair.h | 56 ++++++++++++++++++- .../remove_degeneracies_test.cpp | 15 +++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 02d71ec97a8..d12e1321a95 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -193,6 +193,7 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) /// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// +/// @param e the edge to check whether is degenerate /// @param pm the triangulated surface mesh to be repaired /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// @@ -234,6 +235,57 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript return is_degenerate_edge(e, pm, parameters::all_default()); } +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is degenerate. +/// A triangle face is considered degenerate if all three points of the face are collinear. +/// +/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the face to check whether is degenerate +/// @param tm the triangulated surface mesh to be repaired +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor : +/// - `Collinear_3` to check whether 3 points are collinear +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is degenerate +template +bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, + TriangleMesh& tm, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + // call from BGL helpers + return is_degenerate_triangle_face(f, tm, vpmap, traits); +} + +template +bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, + TriangleMesh& tm) +{ + return CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, tm, parameters::all_default()); +} + // this function remove a border edge even if it does not satisfy the link condition. // The only limitation is that the length connected component of the boundary this edge // is strictly greater than 3 @@ -780,7 +832,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, // Then, remove triangles made of 3 collinear points std::set degenerate_face_set; BOOST_FOREACH(face_descriptor fd, faces(tmesh)) - if ( is_degenerate_triangle_face(fd, tmesh, vpmap, traits) ) + if ( is_degenerate_triangle_face(fd, tmesh) ) degenerate_face_set.insert(fd); nb_deg_faces+=degenerate_face_set.size(); @@ -811,7 +863,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, degenerate_face_set.erase( face(hd2, tmesh) ); // remove the central vertex and check if the new face is degenerated hd=CGAL::Euler::remove_center_vertex(hd, tmesh); - if (is_degenerate_triangle_face(face(hd, tmesh), tmesh, vpmap, traits)) + if (is_degenerate_triangle_face(face(hd, tmesh), tmesh)) { degenerate_face_set.insert( face(hd, tmesh) ); } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 3e069bf79be..e87bd9b2b83 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -44,6 +44,20 @@ void check_edge_degeneracy(const char* fname) CGAL::Polygon_mesh_processing::is_degenerate_edge(e, mesh); } +void check_triangle_face_degeneracy(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) + CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, mesh); +} + int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -54,6 +68,7 @@ int main() fix("data_degeneracies/degtri_single.off"); fix("data_degeneracies/trihole.off"); check_edge_degeneracy("data_degeneracies/degtri_edge.off"); + check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); return 0; } From 9f315abad6d417634a83fce864164c2cceda654f Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 10 Apr 2018 17:40:30 +0200 Subject: [PATCH 03/65] duplicate_vertices function doc --- .../CGAL/Polygon_mesh_processing/repair.h | 40 +++++++++++++++---- .../remove_degeneracies_test.cpp | 14 +++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index d12e1321a95..9b7fcfa4c78 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -157,8 +157,6 @@ struct Less_vertex_point{ } }; -///\cond SKIP_IN_MANUAL - template OutputIterator degenerate_faces(const TriangleMesh& tm, @@ -1381,7 +1379,6 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, return nb_deg_faces; } - template std::size_t remove_degenerate_faces(TriangleMesh& tmesh) { @@ -1389,9 +1386,38 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh) CGAL::Polygon_mesh_processing::parameters::all_default()); } -template -std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, Vpm vpm) +/// \ingroup PMP_repairing_grp +/// duplicates all non-manifold vertices of the input mesh. +/// +/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param tm the triangulated surface mesh to be repaired +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is degenerate +template +std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, + const NamedParameters& np) { + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::type VertexPointMap; + VertexPointMap vpm = choose_param(get_param(np, internal_np::vertex_point), + get_property_map(vertex_point, tm)); + typedef boost::graph_traits GT; typedef typename GT::vertex_descriptor vertex_descriptor; typedef typename GT::halfedge_descriptor halfedge_descriptor; @@ -1443,11 +1469,9 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, Vpm vpm) template std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm) { - return duplicate_non_manifold_vertices(tm, get(vertex_point, tm)); + return duplicate_non_manifold_vertices(tm, parameters::all_default()); } -/// \endcond - /// \ingroup PMP_repairing_grp /// removes the isolated vertices from any polygon mesh. diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index e87bd9b2b83..8f89842f922 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -58,6 +58,19 @@ void check_triangle_face_degeneracy(const char* fname) CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, mesh); } +void test_vetices_duplication(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); +} + int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -69,6 +82,7 @@ int main() fix("data_degeneracies/trihole.off"); check_edge_degeneracy("data_degeneracies/degtri_edge.off"); check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); + test_vetices_duplication("data_degeneracies/degtri_four.off"); return 0; } From c3e7f6d94b09b71bf5df281c7fba50628d9436d0 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Wed, 11 Apr 2018 12:04:21 +0200 Subject: [PATCH 04/65] is_non_manifold_vertex function --- .../CGAL/Polygon_mesh_processing/repair.h | 54 +++++++++++++++---- .../remove_degeneracies_test.cpp | 15 ++++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 9b7fcfa4c78..aef378dee83 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -184,6 +184,40 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) return degenerate_faces(tm, get(vertex_point, tm), Kernel(), out); } +/// \ingroup PMP_repairing_grp +/// checks whether a vertex is non-manifold. +/// +/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// +/// @param v the vertex to check whether is degenerate +/// @param tm the triangulated surface mesh upon evaluation +/// +/// \return true if the vertrex is non-manifold +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const TriangleMesh& tm) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + + boost::unordered_set halfedges_handled; + halfedge_descriptor start = halfedge(v, tm); + halfedge_descriptor h=start; + do{ + halfedges_handled.insert(h); + h=opposite(next(h, tm), tm); + }while(h != start); + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) + { + if(!halfedges_handled.count(h)) + return true; + } + return false; +} + /// \ingroup PMP_repairing_grp /// checks whether an edge is degenerate. /// An edge is considered degenerate if two of its vertices share the same location. @@ -192,7 +226,7 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param e the edge to check whether is degenerate -/// @param pm the triangulated surface mesh to be repaired +/// @param pm the triangulated surface mesh upon evaluation /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -210,15 +244,15 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) /// \return true if the edge is degenerate template bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, - PolygonMesh& pm, + const PolygonMesh& pm, const NamedParameters& np) { using boost::get_param; using boost::choose_param; - typedef typename GetVertexPointMap::type VertexPointMap; + typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_property_map(vertex_point, pm)); + get_const_property_map(vertex_point, pm)); typedef typename GetGeomTraits::type Traits; Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); @@ -228,7 +262,7 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript template bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, - PolygonMesh& pm) + const PolygonMesh& pm) { return is_degenerate_edge(e, pm, parameters::all_default()); } @@ -241,7 +275,7 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param f the face to check whether is degenerate -/// @param tm the triangulated surface mesh to be repaired +/// @param tm the triangulated surface mesh upon evaluation /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -259,7 +293,7 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// \return true if the triangle face is degenerate template bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, - TriangleMesh& tm, + const TriangleMesh& tm, const NamedParameters& np) { CGAL_assertion(CGAL::is_triangle_mesh(tm)); @@ -267,9 +301,9 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac using boost::get_param; using boost::choose_param; - typedef typename GetVertexPointMap::type VertexPointMap; + typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_property_map(vertex_point, tm)); + get_const_property_map(vertex_point, tm)); typedef typename GetGeomTraits::type Traits; Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); @@ -279,7 +313,7 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac template bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, - TriangleMesh& tm) + const TriangleMesh& tm) { return CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, tm, parameters::all_default()); } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 8f89842f922..6f2c49341d9 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -71,6 +71,20 @@ void test_vetices_duplication(const char* fname) CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); } +void test_vertex_non_manifoldness(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + BOOST_FOREACH(typename boost::graph_traits::vertex_descriptor v, vertices(mesh)) + CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh); +} + int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -83,6 +97,7 @@ int main() check_edge_degeneracy("data_degeneracies/degtri_edge.off"); check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); test_vetices_duplication("data_degeneracies/degtri_four.off"); + test_vertex_non_manifoldness("data/non_manifold_vertex.off");; return 0; } From 1f0628fad247d3f7b7c199964b0b4a71f18b5176 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Thu, 12 Apr 2018 10:24:48 +0200 Subject: [PATCH 05/65] is needle andcap functions --- .../CGAL/Polygon_mesh_processing/repair.h | 149 ++++++++++++++++++ .../remove_degeneracies_test.cpp | 49 +++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index aef378dee83..d13eff11f9f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -318,6 +318,155 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac return CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, tm, parameters::all_default()); } +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is needle-like. +/// In a needle-like triangle its longest edge is much longer than the shortest one. +/// +/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the face to check whether is almost degenerate +/// @param tm the triangulated surface mesh upon evaluation +/// @param threshold a number in the range [0, 1] to indicate the tolerance +/// upon which to characterize the degeneracy. 1 means that needle triangles +/// are those that have a infinitely small edge, while 0 means that needle triangles +/// are those that would have an infinitely long edge +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is almost degenerate +template +bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type FT; + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GT::vertex_descriptor vertex_descriptor; + typedef typename boost::property_traits::value_type Point; + typedef typename Kernel_traits::Kernel::Vector_3 Vector; + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + { + vertex_descriptor v0 = source(h, tm); + vertex_descriptor v1 = target(h, tm); + vertex_descriptor v2 = target(next(h, tm), tm); + + Vector a = get(vpmap, v0) - get(vpmap, v1); + Vector b = get(vpmap, v2) - get(vpmap, v1); + double ab = a*b; + double aa = a.squared_length(); + double bb = b.squared_length(); + double dot_ab = a*b / (CGAL::sqrt(aa) * CGAL::sqrt(bb)); + + // threshold = 1 means no tolerance, totally degenerate + if(dot_ab > threshold) + return true; + } + return false; +} + +template +bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) +{ + is_needle_triangle_face(f, tm, threshold, parameters::all_default()); +} + +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is cap-like. +/// A cap-like triangle has an angle very close to 180 degrees. +/// +/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the face to check whether is almost degenerate +/// @param tm the triangulated surface mesh upon evaluation +/// @param threshold a number in the range [0, 1] to indicate the tolerance +/// upon which to characterize the degeneracy. 1 means that cap triangles +/// are considered those whose vertices for an angle of 180 degrees, while 0 means that +/// all triangles are considered caps. +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is almost degenerate +template +bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type FT; + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GT::vertex_descriptor vertex_descriptor; + typedef typename boost::property_traits::value_type Point; + typedef typename Kernel_traits::Kernel::Vector_3 Vector; + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + { + vertex_descriptor v0 = source(h, tm); + vertex_descriptor v1 = target(h, tm); + vertex_descriptor v2 = target(next(h, tm), tm); + + Vector a = get(vpmap, v0) - get(vpmap, v1); + Vector b = get(vpmap, v2) - get(vpmap, v1); + double ab = a*b; + double aa = a.squared_length(); + double bb = b.squared_length(); + double dot_ab = a*b / (CGAL::sqrt(aa) * CGAL::sqrt(bb)); + + // threshold = 1 means no tolerance, totally degenerate + // take the opposite, because cos it -1 at 180 degrees + if(dot_ab < -threshold) + return true; + } + return false; +} + +template +bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) +{ + is_cap_triangle_face(f, tm, threshold, parameters::all_default()); +} + // this function remove a border edge even if it does not satisfy the link condition. // The only limitation is that the length connected component of the boundary this edge // is strictly greater than 3 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 6f2c49341d9..82c275c9702 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -82,9 +82,52 @@ void test_vertex_non_manifoldness(const char* fname) } BOOST_FOREACH(typename boost::graph_traits::vertex_descriptor v, vertices(mesh)) - CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh); + { + if(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)) + std::cout << "true\n"; + + } } +void test_needle(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + double threshold = 0.8; + + BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) + { + if(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)) + std::cout << "needle\n"; + } +} + +void test_cap(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + double threshold = 0.8; + + BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) + { + if(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)) + std::cout << "cap\n"; + } +} + + int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -97,7 +140,9 @@ int main() check_edge_degeneracy("data_degeneracies/degtri_edge.off"); check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); test_vetices_duplication("data_degeneracies/degtri_four.off"); - test_vertex_non_manifoldness("data/non_manifold_vertex.off");; + test_vertex_non_manifoldness("data/non_manifold_vertex.off"); + test_needle("data_degeneracies/needle.off"); + test_cap("data_degeneracies/cap.off"); return 0; } From b4da4a21540f38a388f7560e74d00e0c4a263363 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Thu, 12 Apr 2018 17:27:19 +0200 Subject: [PATCH 06/65] add a couple of tests to cmakelists --- .../test/Polygon_mesh_processing/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt index f7af9794708..5ab90c954a9 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt @@ -100,6 +100,8 @@ endif() create_single_source_cgal_program("surface_intersection_sm_poly.cpp" ) create_single_source_cgal_program("test_orient_cc.cpp") create_single_source_cgal_program("test_pmp_transform.cpp") + create_single_source_cgal_program("remove_degeneracies_test.cpp") + create_single_source_cgal_program("remove_identical_test.cpp") if( TBB_FOUND ) CGAL_target_use_TBB(test_pmp_distance) From c79add2c6a65b1ba5b54312ad3359653d383fc1a Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Thu, 12 Apr 2018 17:31:42 +0200 Subject: [PATCH 07/65] merge vertices, tests & data --- .../Polygon_mesh_processing/stitch_holes.h | 104 ++++++++++++++++++ .../data/merge_points.off | 10 ++ .../data_degeneracies/degtri_edge.off | 6 + .../remove_identical_test.cpp | 75 +++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/degtri_edge.off create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h new file mode 100644 index 00000000000..af71d0bf408 --- /dev/null +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h @@ -0,0 +1,104 @@ +#ifndef CGAL_STITCH_HOLES_H +#define CGAL_STITCH_HOLES _H + + +#include +#include +#include +#include + + +namespace CGAL{ + +namespace Polygon_mesh_processing{ + + +template +void extract_connected_components(PolygonMesh& mesh, + OutputIterator out) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + + std::set border_halfedges; + + BOOST_FOREACH(halfedge_descriptor h, halfedges(mesh)) + { + if(is_border(h, mesh)) + border_halfedges.insert(h); + } + + std::set connected_component; + BOOST_FOREACH(halfedge_descriptor h, border_halfedges) + { + if(connected_component.insert(h).second) + { + halfedge_descriptor start = h; + do{ + h = next(h, mesh); + connected_component.insert(h); + } while(h != start); + + *out++=connected_component; + } + } +} + + +template +std::size_t count_identical_points(PolygonMesh& mesh, + std::vector cc_list) +{ + // cc is a std::vector > + + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + + typedef typename boost::property_map::const_type Vpm; + Vpm vpm = get(boost::vertex_point, mesh); + + for(std::set i_cc : cc_list) + { + // for each cc + BOOST_FOREACH(halfedge_descriptor h, i_cc) + { + vertex_descriptor vs = source(h, mesh); + vertex_descriptor vt = target(h, mesh); + + // find identicals + } + } + return 0; // how many found +} + +/// \ingroup PMP_repairing_grp +/// merges two vertices into one +/// +/// @tparam TriangleMesh a model of `FaceListGraph` +/// +/// @param mesh the input triangle mesh +/// @param v_keep the vertex to be kept +/// @param v_rm the vertex to be removed +template +void merge_identical_points(PolygonMesh& mesh, + typename boost::graph_traits::vertex_descriptor v_keep, + typename boost::graph_traits::vertex_descriptor v_rm) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + halfedge_descriptor h = halfedge(v_rm, mesh); + halfedge_descriptor start = h; + + do{ + set_target(h, v_keep, mesh); + h = opposite(next(h, mesh), mesh); + } while( h != start ); + + remove_vertex(v_rm, mesh); +} + + + + +} +} + +#endif //CGAL_STITCH_HOLES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off new file mode 100644 index 00000000000..d520c25cdf2 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off @@ -0,0 +1,10 @@ +OFF +6 2 0 +0 0 0 +1 0 0 +1 0 0 +2 0 0 +0.5 1 0 +1.5 1 0 +3 0 1 4 +3 2 3 5 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/degtri_edge.off b/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/degtri_edge.off new file mode 100644 index 00000000000..afd48de232f --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/degtri_edge.off @@ -0,0 +1,6 @@ +OFF +3 1 0 +0 0 0 +1 0 0 +0 0 0 +3 0 1 2 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp new file mode 100644 index 00000000000..ae31ad7f8c8 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include + +#include + + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Surface_mesh Surface_mesh; + + +void test_connected_components(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + + + std::vector > connected_components; + + CGAL::Polygon_mesh_processing::extract_connected_components(mesh, + std::back_inserter(connected_components)); + + std::cout << "# cc = " << connected_components.size(); + + for(auto set : connected_components) + { + std::cout << "of size= " << set.size() << std::endl; + } +} + + +void test_merge_points(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::vector verts(vertices(mesh).begin(), vertices(mesh).end()); + + vertex_descriptor v_rm = verts[1]; + vertex_descriptor v_keep = verts[2]; + + CGAL::Polygon_mesh_processing::merge_identical_points(mesh, v_keep, v_rm); + + std::ofstream out("/tmp/result.off"); + out << mesh; + out.close(); +} + + +int main() +{ + + + test_connected_components("data/small_ex.off"); + test_merge_points("data/merge_points.off"); + + + + return 0; +} From af6576047505933b2f8d9dc6e5c5c702e4a5814e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 13 Apr 2018 13:44:53 +0200 Subject: [PATCH 08/65] rewrite boundary cycle merging --- .../Polygon_mesh_processing/stitch_holes.h | 203 ++++++++++++------ .../data/merge_points.off | 103 ++++++++- .../remove_identical_test.cpp | 60 ++---- 3 files changed, 243 insertions(+), 123 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h index af71d0bf408..eb2010ed104 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h @@ -1,104 +1,171 @@ -#ifndef CGAL_STITCH_HOLES_H -#define CGAL_STITCH_HOLES _H +// Copyright (c) 2018 GeometryFactory (France). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// You can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Licensees holding a valid commercial license may use this file in +// accordance with the commercial license agreement provided with the software. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: GPL-3.0+ +// +// +// Author(s) : Sebastien Loriot +#ifndef CGAL_STITCH_HOLES_H +#define CGAL_STITCH_HOLES_H #include #include #include #include +#include +#include +#include namespace CGAL{ namespace Polygon_mesh_processing{ - +/// \todo document me +/// It should probably go into BGL package template -void extract_connected_components(PolygonMesh& mesh, - OutputIterator out) +OutputIterator +extract_boundary_cycles(PolygonMesh& pm, + OutputIterator out) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - std::set border_halfedges; - - BOOST_FOREACH(halfedge_descriptor h, halfedges(mesh)) + boost::unordered_set hedge_handled; + BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) { - if(is_border(h, mesh)) - border_halfedges.insert(h); - } - - std::set connected_component; - BOOST_FOREACH(halfedge_descriptor h, border_halfedges) - { - if(connected_component.insert(h).second) + if(is_border(h, pm) && hedge_handled.insert(h).second) { - halfedge_descriptor start = h; - do{ - h = next(h, mesh); - connected_component.insert(h); - } while(h != start); - - *out++=connected_component; + *out++=h; + BOOST_FOREACH(halfedge_descriptor h2, halfedges_around_face(h, pm)) + hedge_handled.insert(h2); } } -} - - -template -std::size_t count_identical_points(PolygonMesh& mesh, - std::vector cc_list) -{ - // cc is a std::vector > - - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - - typedef typename boost::property_map::const_type Vpm; - Vpm vpm = get(boost::vertex_point, mesh); - - for(std::set i_cc : cc_list) - { - // for each cc - BOOST_FOREACH(halfedge_descriptor h, i_cc) - { - vertex_descriptor vs = source(h, mesh); - vertex_descriptor vt = target(h, mesh); - - // find identicals - } - } - return 0; // how many found + return out; } /// \ingroup PMP_repairing_grp -/// merges two vertices into one -/// -/// @tparam TriangleMesh a model of `FaceListGraph` -/// -/// @param mesh the input triangle mesh -/// @param v_keep the vertex to be kept -/// @param v_rm the vertex to be removed -template -void merge_identical_points(PolygonMesh& mesh, - typename boost::graph_traits::vertex_descriptor v_keep, - typename boost::graph_traits::vertex_descriptor v_rm) +/// \todo document me +template +void merge_vertices(const VertexRange& vertices, + PolygonMesh& pm) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - halfedge_descriptor h = halfedge(v_rm, mesh); - halfedge_descriptor start = h; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + vertex_descriptor v_kept=*boost::begin(vertices); + std::vector vertices_to_rm; + + BOOST_FOREACH(vertex_descriptor vd, vertices) + { + if (vd==v_kept) continue; // skip identical vertices + if (edge(vd, v_kept, pm).second) continue; // skip null edges + bool shall_continue=false; + BOOST_FOREACH(halfedge_descriptor hd, halfedges_around_target(v_kept, pm)) + { + if (edge(vd, source(hd, pm), pm).second) + { + shall_continue=true; + break; + } + } + if (shall_continue) continue; // skip vertices already incident to the same vertex + + internal::update_target_vertex(halfedge(vd, pm), v_kept, pm); + vertices_to_rm.push_back(vd); + } + + BOOST_FOREACH(vertex_descriptor vd, vertices_to_rm) + remove_vertex(vd, pm); +} + +/// \ingroup PMP_repairing_grp +/// \todo document me +template +void merge_duplicated_vertices_in_boundary_cycle(typename boost::graph_traits::halfedge_descriptor h, + PolygonMesh& pm, + const NamedParameter& np) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef typename GetVertexPointMap::const_type Vpm; + typedef typename boost::property_traits::value_type Point_3; + Vpm vpm = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, pm)); + + // collect all the vertices of the cycle + std::vector vertices; + halfedge_descriptor start=h; do{ - set_target(h, v_keep, mesh); - h = opposite(next(h, mesh), mesh); - } while( h != start ); + vertices.push_back(target(h,pm)); + h=next(h, pm); + }while(start!=h); - remove_vertex(v_rm, mesh); + // sort vertices using their point to ease the detection + // of vertices with identical points + CGAL::Property_map_to_unary_function Get_point(vpm); + std::sort( vertices.begin(), vertices.end(), + boost::bind(std::less(), boost::bind(Get_point,_1), + boost::bind(Get_point, _2)) ); + std::size_t nbv=vertices.size(); + std::size_t i=1; + + std::vector< std::vector > identical_vertices; + while(i!=nbv) + { + if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) + { + identical_vertices.push_back( std::vector() ); + identical_vertices.back().push_back(vertices[i-1]); + identical_vertices.back().push_back(vertices[i]); + while(++i!=nbv) + { + if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) + identical_vertices.back().push_back(vertices[i]); + else + break; + } + } + ++i; + } + BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) + merge_vertices(vrtcs, pm); } +/// \ingroup PMP_repairing_grp +/// \todo document me +template +void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, + const NamedParameter& np) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + std::vector cycles; + extract_boundary_cycles(pm, std::back_inserter(cycles)); - + BOOST_FOREACH(halfedge_descriptor h, cycles) + merge_duplicated_vertices_in_boundary_cycle(h, pm, np); } + +template +void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) +{ + merge_duplicated_vertices_in_boundary_cycles(pm, parameters::all_default()); } +} } // end of CGAL::Polygon_mesh_processing + #endif //CGAL_STITCH_HOLES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off index d520c25cdf2..7a83712189f 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/merge_points.off @@ -1,10 +1,95 @@ OFF -6 2 0 -0 0 0 -1 0 0 -1 0 0 -2 0 0 -0.5 1 0 -1.5 1 0 -3 0 1 4 -3 2 3 5 +47 45 0 + +-0.026804100000000001 -0.30492799999999998 0.142542 +-0.018234400000000001 -0.28125499999999998 0.15192700000000001 +-0.074329900000000004 -0.27701599999999998 0.17852999999999999 +-0.075232300000000002 -0.238787 0.19677600000000001 +-0.092496499999999995 -0.26196000000000003 0.18681900000000001 +-0.107975 -0.220055 0.202264 +-0.098042799999999999 -0.24015500000000001 0.197738 +-0.076830499999999996 -0.20389099999999999 0.20217599999999999 +-0.10664999999999999 -0.18507100000000001 0.204627 +-0.048507700000000001 -0.30671799999999999 0.14971999999999999 +-0.10985300000000001 -0.27012399999999998 0.16877500000000001 +0.020159400000000001 -0.26358599999999999 0.11985899999999999 +-0.0820192 -0.303394 0.145977 +-0.11151800000000001 -0.28692499999999999 0.148566 +0.033791700000000001 -0.209923 0.115845 +0.019949499999999998 -0.19298699999999999 0.125832 +0.00080011299999999997 -0.19967099999999999 0.13874900000000001 +0.0102764 -0.16375000000000001 0.13344700000000001 +0.015325999999999999 -0.12934699999999999 0.14213100000000001 +0.043303899999999999 -0.239647 0.099889199999999997 +-0.010328800000000001 -0.10643 0.14183100000000001 +-0.032521000000000001 -0.10838299999999999 0.13683899999999999 +-0.085985000000000006 -0.101282 0.15809300000000001 +-0.108849 -0.11043600000000001 0.16941700000000001 +-0.052558500000000001 -0.097836599999999996 0.13975499999999999 +-0.10316599999999999 -0.15746599999999999 0.19631499999999999 +-0.112763 -0.133107 0.183753 +-0.063495300000000005 -0.15489 0.17952499999999999 +-0.064211199999999996 -0.12604799999999999 0.16286 +-0.086169300000000004 -0.13996600000000001 0.183699 +-0.063495300000000005 -0.15489 0.17952499999999999 +-0.023309900000000001 -0.17152600000000001 0.15354100000000001 +-0.0254547 -0.133829 0.14063300000000001 +-0.079254099999999994 -0.17391000000000001 0.197354 +-0.0458796 -0.210094 0.18626999999999999 +-0.063495300000000005 -0.15489 0.17952499999999999 +-0.0097084900000000002 -0.22793099999999999 0.15407299999999999 +-0.0458796 -0.210094 0.18626999999999999 +-0.0458796 -0.210094 0.18626999999999999 +-0.075232300000000002 -0.238787 0.19677600000000001 +-0.049308200000000003 -0.25528899999999999 0.18343899999999999 +-0.0218198 -0.25775500000000001 0.16445000000000001 +-0.049308200000000003 -0.25528899999999999 0.18343899999999999 +-0.041782300000000001 -0.28198800000000002 0.16825000000000001 +0.00026201499999999999 -0.25824399999999997 0.14286599999999999 +0.015174200000000001 -0.229959 0.128466 +-0.0097084900000000002 -0.22793099999999999 0.15407299999999999 +3 43 0 1 +3 2 3 4 +3 5 6 3 +3 7 5 3 +3 5 7 8 +3 0 43 9 +3 10 2 4 +3 1 44 41 +3 11 45 44 +3 12 9 2 +3 13 12 2 +3 14 15 45 +3 16 45 15 +3 43 1 41 +3 17 16 15 +3 16 31 36 +3 31 16 17 +3 17 18 32 +3 11 19 45 +3 43 2 9 +3 18 20 32 +3 20 21 32 +3 13 2 10 +3 29 28 22 +3 45 19 14 +3 23 29 22 +3 24 22 28 +3 21 24 28 +3 32 21 28 +3 33 29 25 +3 8 33 25 +3 8 7 33 +3 26 29 23 +3 34 33 7 +3 25 29 26 +3 4 3 6 +3 31 17 32 +3 29 27 28 +3 32 30 31 +3 34 35 33 +3 31 37 36 +3 40 38 39 +3 41 42 43 +3 45 46 44 +3 44 46 41 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp index ae31ad7f8c8..5e65212e9b4 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp @@ -11,7 +11,8 @@ typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef CGAL::Surface_mesh Surface_mesh; -void test_connected_components(const char* fname) +void test_merge_duplicated_vertices_in_boundary_cycles(const char* fname, + std::size_t expected_nb_vertices) { std::ifstream input(fname); @@ -21,55 +22,22 @@ void test_connected_components(const char* fname) exit(1); } - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + std::cout << "Testing " << fname << "\n"; + std::cout << " input mesh has " << vertices(mesh).size() << " vertices.\n"; + CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles(mesh); + std::cout << " output mesh has " << vertices(mesh).size() << " vertices.\n"; - - std::vector > connected_components; - - CGAL::Polygon_mesh_processing::extract_connected_components(mesh, - std::back_inserter(connected_components)); - - std::cout << "# cc = " << connected_components.size(); - - for(auto set : connected_components) - { - std::cout << "of size= " << set.size() << std::endl; - } + assert(expected_nb_vertices==0 || + expected_nb_vertices == vertices(mesh).size()); } -void test_merge_points(const char* fname) +int main(int argc, char** argv) { - std::ifstream input(fname); - - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - std::vector verts(vertices(mesh).begin(), vertices(mesh).end()); - - vertex_descriptor v_rm = verts[1]; - vertex_descriptor v_keep = verts[2]; - - CGAL::Polygon_mesh_processing::merge_identical_points(mesh, v_keep, v_rm); - - std::ofstream out("/tmp/result.off"); - out << mesh; - out.close(); -} - - -int main() -{ - - - test_connected_components("data/small_ex.off"); - test_merge_points("data/merge_points.off"); - - - + if (argc==1) + test_merge_duplicated_vertices_in_boundary_cycles("data/merge_points.off", 43); + else + for (int i=1; i< argc; ++i) + test_merge_duplicated_vertices_in_boundary_cycles(argv[i], 0); return 0; } From e1f0740b533159e4510aaa1e8a8c76bd1e4d0dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 13 Apr 2018 14:00:55 +0200 Subject: [PATCH 09/65] rename header and test file --- .../{stitch_holes.h => merge_border_vertices.h} | 6 +++--- .../test/Polygon_mesh_processing/CMakeLists.txt | 2 +- ..._identical_test.cpp => test_merging_border_vertices.cpp} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{stitch_holes.h => merge_border_vertices.h} (96%) rename Polygon_mesh_processing/test/Polygon_mesh_processing/{remove_identical_test.cpp => test_merging_border_vertices.cpp} (95%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h similarity index 96% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index eb2010ed104..7ff0c1ce6e1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/stitch_holes.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -19,8 +19,8 @@ // // Author(s) : Sebastien Loriot -#ifndef CGAL_STITCH_HOLES_H -#define CGAL_STITCH_HOLES_H +#ifndef CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H +#define CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H #include #include @@ -168,4 +168,4 @@ void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) } } // end of CGAL::Polygon_mesh_processing -#endif //CGAL_STITCH_HOLES_H +#endif //CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt index 5ab90c954a9..ee3d82c4fe8 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt @@ -101,7 +101,7 @@ endif() create_single_source_cgal_program("test_orient_cc.cpp") create_single_source_cgal_program("test_pmp_transform.cpp") create_single_source_cgal_program("remove_degeneracies_test.cpp") - create_single_source_cgal_program("remove_identical_test.cpp") + create_single_source_cgal_program("test_merging_border_vertices.cpp") if( TBB_FOUND ) CGAL_target_use_TBB(test_pmp_distance) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp similarity index 95% rename from Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp rename to Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp index 5e65212e9b4..fc749d88373 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_identical_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include From 0830c7a112f2af0a622014aa7073ebfdfd42e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 13 Apr 2018 14:09:50 +0200 Subject: [PATCH 10/65] add missing overload --- .../CGAL/Polygon_mesh_processing/merge_border_vertices.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 7ff0c1ce6e1..8b78ff256e5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -166,6 +166,14 @@ void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) merge_duplicated_vertices_in_boundary_cycles(pm, parameters::all_default()); } +template +void merge_duplicated_vertices_in_boundary_cycle( + typename boost::graph_traits::halfedge_descriptor h, + PolygonMesh& pm) +{ + merge_duplicated_vertices_in_boundary_cycles(h, pm, parameters::all_default()); +} + } } // end of CGAL::Polygon_mesh_processing #endif //CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H From fe407a701fa6d149abaf90b8c9b16a26f9abca4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 13 Apr 2018 14:54:29 +0200 Subject: [PATCH 11/65] add a function to merge vertices globally --- .../merge_border_vertices.h | 113 +++++++++++++----- .../test_merging_border_vertices.cpp | 44 ++++++- 2 files changed, 126 insertions(+), 31 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 8b78ff256e5..76a0d132cca 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -35,6 +35,46 @@ namespace CGAL{ namespace Polygon_mesh_processing{ +namespace internal { + +// warning: vertices will be altered (sorted) +template +void detect_identical_vertices(std::vector& vertices, + std::vector< std::vector >& identical_vertices, + Vpm vpm) +{ + typedef typename boost::property_traits::value_type Point_3; + + // sort vertices using their point to ease the detection + // of vertices with identical points + CGAL::Property_map_to_unary_function Get_point(vpm); + std::sort( vertices.begin(), vertices.end(), + boost::bind(std::less(), boost::bind(Get_point,_1), + boost::bind(Get_point, _2)) ); + std::size_t nbv=vertices.size(); + std::size_t i=1; + + while(i!=nbv) + { + if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) + { + identical_vertices.push_back( std::vector() ); + identical_vertices.back().push_back(vertices[i-1]); + identical_vertices.back().push_back(vertices[i]); + while(++i!=nbv) + { + if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) + identical_vertices.back().push_back(vertices[i]); + else + break; + } + } + ++i; + } +} + +} // end of internal + /// \todo document me /// It should probably go into BGL package template @@ -60,8 +100,8 @@ extract_boundary_cycles(PolygonMesh& pm, /// \ingroup PMP_repairing_grp /// \todo document me template -void merge_vertices(const VertexRange& vertices, - PolygonMesh& pm) +void merge_boundary_vertices(const VertexRange& vertices, + PolygonMesh& pm) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; @@ -102,7 +142,7 @@ void merge_duplicated_vertices_in_boundary_cycle(typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename GetVertexPointMap::const_type Vpm; - typedef typename boost::property_traits::value_type Point_3; + Vpm vpm = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, pm)); @@ -114,35 +154,11 @@ void merge_duplicated_vertices_in_boundary_cycle(typename boost::graph_traits Get_point(vpm); - std::sort( vertices.begin(), vertices.end(), - boost::bind(std::less(), boost::bind(Get_point,_1), - boost::bind(Get_point, _2)) ); - std::size_t nbv=vertices.size(); - std::size_t i=1; - std::vector< std::vector > identical_vertices; - while(i!=nbv) - { - if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) - { - identical_vertices.push_back( std::vector() ); - identical_vertices.back().push_back(vertices[i-1]); - identical_vertices.back().push_back(vertices[i]); - while(++i!=nbv) - { - if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) - identical_vertices.back().push_back(vertices[i]); - else - break; - } - } - ++i; - } + internal::detect_identical_vertices(vertices, identical_vertices, vpm); + BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) - merge_vertices(vrtcs, pm); + merge_boundary_vertices(vrtcs, pm); } /// \ingroup PMP_repairing_grp @@ -160,6 +176,36 @@ void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, merge_duplicated_vertices_in_boundary_cycle(h, pm, np); } + +/// \ingroup PMP_repairing_grp +/// \todo document me +template +void merge_duplicated_boundary_vertices( PolygonMesh& pm, + const NamedParameter& np) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef typename GetVertexPointMap::const_type Vpm; + + Vpm vpm = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, pm)); + + std::vector border_vertices; + BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) + { + if(is_border(h, pm)) + border_vertices.push_back(target(h, pm)); + } + + std::vector< std::vector > identical_vertices; + internal::detect_identical_vertices(border_vertices, identical_vertices, vpm); + + BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) + merge_boundary_vertices(vrtcs, pm); +} + + + template void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) { @@ -174,6 +220,13 @@ void merge_duplicated_vertices_in_boundary_cycle( merge_duplicated_vertices_in_boundary_cycles(h, pm, parameters::all_default()); } +template +void merge_duplicated_boundary_vertices(PolygonMesh& pm) +{ + merge_duplicated_boundary_vertices(pm, parameters::all_default()); +} + + } } // end of CGAL::Polygon_mesh_processing #endif //CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp index fc749d88373..1abc453207a 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp @@ -22,22 +22,64 @@ void test_merge_duplicated_vertices_in_boundary_cycles(const char* fname, exit(1); } - std::cout << "Testing " << fname << "\n"; + std::cout << "Testing merging in cycles " << fname << "\n"; std::cout << " input mesh has " << vertices(mesh).size() << " vertices.\n"; CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles(mesh); std::cout << " output mesh has " << vertices(mesh).size() << " vertices.\n"; assert(expected_nb_vertices==0 || expected_nb_vertices == vertices(mesh).size()); + if (expected_nb_vertices==0) + { + std::cout << "writting output to out1.off\n"; + std::ofstream output("out1.off"); + output << std::setprecision(17); + output << mesh; + } +} + +void test_merge_duplicated_boundary_vertices(const char* fname, + std::size_t expected_nb_vertices) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + std::cout << "Testing merging globally " << fname << "\n"; + std::cout << " input mesh has " << vertices(mesh).size() << " vertices.\n"; + CGAL::Polygon_mesh_processing::merge_duplicated_boundary_vertices(mesh); + std::cout << " output mesh has " << vertices(mesh).size() << " vertices.\n"; + + assert(expected_nb_vertices == 0 || + expected_nb_vertices == vertices(mesh).size()); + if (expected_nb_vertices==0) + { + std::cout << "writting output to out2.off\n"; + std::ofstream output("out2.off"); + output << std::setprecision(17); + output << mesh; + } } int main(int argc, char** argv) { if (argc==1) + { test_merge_duplicated_vertices_in_boundary_cycles("data/merge_points.off", 43); + test_merge_duplicated_boundary_vertices("data/merge_points.off", 40); + } else + { for (int i=1; i< argc; ++i) + { test_merge_duplicated_vertices_in_boundary_cycles(argv[i], 0); + test_merge_duplicated_boundary_vertices(argv[i], 0); + } + } return 0; } From e6ffc5f505ea2d58e5eef4a972e2523b6c85c519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 13 Apr 2018 16:25:09 +0200 Subject: [PATCH 12/65] remove incorrect optimisation --- .../CGAL/Polygon_mesh_processing/merge_border_vertices.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 76a0d132cca..3435e5fd7bd 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -66,10 +66,14 @@ void detect_identical_vertices(std::vector& vertices, if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) identical_vertices.back().push_back(vertices[i]); else + { + ++i; break; + } } } - ++i; + else + ++i; } } From 903df8106a9f7e3f3bbf24be8d371366fed43223 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Mon, 16 Apr 2018 12:05:03 +0200 Subject: [PATCH 13/65] corrections after the review --- BGL/include/CGAL/boost/graph/helpers.h | 27 --- .../CGAL/Polygon_mesh_processing/repair.h | 195 +++++++++++------- .../data/non_manifold_vertex_duplicated.off | 20 ++ .../remove_degeneracies_test.cpp | 69 ++++--- 4 files changed, 190 insertions(+), 121 deletions(-) create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/data/non_manifold_vertex_duplicated.off diff --git a/BGL/include/CGAL/boost/graph/helpers.h b/BGL/include/CGAL/boost/graph/helpers.h index f34e24b7687..d84e1b5c6c3 100644 --- a/BGL/include/CGAL/boost/graph/helpers.h +++ b/BGL/include/CGAL/boost/graph/helpers.h @@ -973,33 +973,6 @@ make_tetrahedron(const P& p0, const P& p1, const P& p2, const P& p3, Graph& g) return opposite(h2,g); } -/// \cond SKIP_IN_DOC -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::halfedge_descriptor hd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - CGAL_assertion(!is_border(hd, tmesh)); - - const typename Traits::Point_3& p1 = get(vpmap, target( hd, tmesh) ); - const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tmesh), tmesh) ); - const typename Traits::Point_3& p3 = get(vpmap, source( hd, tmesh) ); - return traits.collinear_3_object()(p1, p2, p3); -} - -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::face_descriptor fd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - return is_degenerate_triangle_face(halfedge(fd,tmesh), tmesh, vpmap, traits); -} -/// \endcond - /** * \ingroup PkgBGLHelperFct * \brief Creates a triangulated regular prism, outward oriented, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index d13eff11f9f..27ea6f5d191 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -187,46 +187,46 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) /// \ingroup PMP_repairing_grp /// checks whether a vertex is non-manifold. /// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` /// /// @param v the vertex to check whether is degenerate -/// @param tm the triangulated surface mesh upon evaluation +/// @param tm triangle mesh containing v /// /// \return true if the vertrex is non-manifold -template -bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, - const TriangleMesh& tm) +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const PolygonMesh& tm) { CGAL_assertion(CGAL::is_triangle_mesh(tm)); - typedef boost::graph_traits GT; + typedef boost::graph_traits GT; typedef typename GT::halfedge_descriptor halfedge_descriptor; boost::unordered_set halfedges_handled; - halfedge_descriptor start = halfedge(v, tm); - halfedge_descriptor h=start; - do{ - halfedges_handled.insert(h); - h=opposite(next(h, tm), tm); - }while(h != start); BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) + halfedges_handled.insert(h); + + BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) { - if(!halfedges_handled.count(h)) - return true; + if(v == target(h, tm)) + { + if(halfedges_handled.count(h) == 0) + return true; + } } return false; } /// \ingroup PMP_repairing_grp /// checks whether an edge is degenerate. -/// An edge is considered degenerate if two of its vertices share the same location. +/// An edge is considered degenerate if the points of its vertices are identical. /// -/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam PolygonMesh a model of `HalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param e the edge to check whether is degenerate -/// @param pm the triangulated surface mesh upon evaluation +/// @param pm polygon mesh containing e /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -258,6 +258,7 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript if ( traits.equal_3_object()(get(vpmap, target(e, pm)), get(vpmap, source(e, pm))) ) return true; + return false; } template @@ -267,21 +268,48 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript return is_degenerate_edge(e, pm, parameters::all_default()); } +/// \cond SKIP_IN_DOC +template +bool is_degenerate_triangle_face( + typename boost::graph_traits::halfedge_descriptor hd, + TriangleMesh& tmesh, + const VertexPointMap& vpmap, + const Traits& traits) +{ + CGAL_assertion(!is_border(hd, tmesh)); + + const typename Traits::Point_3& p1 = get(vpmap, target( hd, tmesh) ); + const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tmesh), tmesh) ); + const typename Traits::Point_3& p3 = get(vpmap, source( hd, tmesh) ); + return traits.collinear_3_object()(p1, p2, p3); +} + +template +bool is_degenerate_triangle_face( + typename boost::graph_traits::face_descriptor fd, + TriangleMesh& tmesh, + const VertexPointMap& vpmap, + const Traits& traits) +{ + return is_degenerate_triangle_face(halfedge(fd,tmesh), tmesh, vpmap, traits); +} +/// \endcond + /// \ingroup PMP_repairing_grp /// checks whether a triangle face is degenerate. -/// A triangle face is considered degenerate if all three points of the face are collinear. +/// A triangle face is degenerate if its points are collinear. /// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param f the face to check whether is degenerate -/// @param tm the triangulated surface mesh upon evaluation +/// @param tm triangle mesh containing f /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin /// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. /// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. /// The traits class must provide the nested type `Point_3`, @@ -319,26 +347,27 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac } /// \ingroup PMP_repairing_grp -/// checks whether a triangle face is needle-like. -/// In a needle-like triangle its longest edge is much longer than the shortest one. +/// checks whether a triangle face is needle. +/// A triangle is needle if its longest edge is much longer than the shortest one. /// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f the face to check whether is almost degenerate -/// @param tm the triangulated surface mesh upon evaluation +/// @param f a face to check whether is almost degenerate +/// @param tm triangle mesh containing f /// @param threshold a number in the range [0, 1] to indicate the tolerance /// upon which to characterize the degeneracy. 1 means that needle triangles -/// are those that have a infinitely small edge, while 0 means that needle triangles -/// are those that would have an infinitely long edge +/// are those that have a infinitely small edge, while 0 means that all +/// triangles are considered needles. /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin /// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. /// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`. /// \cgalParamEnd /// \cgalNamedParamsEnd /// @@ -357,30 +386,58 @@ bool is_needle_triangle_face(typename boost::graph_traits::face_de typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type FT; + typedef typename GetGeomTraits::type Traits; + typedef typename Traits::FT FT; typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::value_type Point; - typedef typename Kernel_traits::Kernel::Vector_3 Vector; + typedef typename boost::property_traits::reference Point_ref; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + vertex_descriptor v0 = target(halfedge(f, tm), tm); + vertex_descriptor v1 = target(next(halfedge(f, tm), tm), tm); + vertex_descriptor v2 = target(next(next(halfedge(f, tm), tm), tm), tm); + Point_ref p0 = get(vpmap, v0); + Point_ref p1 = get(vpmap, v1); + Point_ref p2 = get(vpmap, v2); + + // e1 = p0p1 e2 = p1p2 e3 = p2p3 + FT e1 = CGAL::squared_distance(p0,p1); + FT e2 = CGAL::squared_distance(p1,p2); + FT e3 = CGAL::squared_distance(p2,p0); + + FT smallest, largest; + if(e1 < e2) { - vertex_descriptor v0 = source(h, tm); - vertex_descriptor v1 = target(h, tm); - vertex_descriptor v2 = target(next(h, tm), tm); - - Vector a = get(vpmap, v0) - get(vpmap, v1); - Vector b = get(vpmap, v2) - get(vpmap, v1); - double ab = a*b; - double aa = a.squared_length(); - double bb = b.squared_length(); - double dot_ab = a*b / (CGAL::sqrt(aa) * CGAL::sqrt(bb)); - - // threshold = 1 means no tolerance, totally degenerate - if(dot_ab > threshold) - return true; + if(e1 < e3) + smallest = e1; + else + smallest = e3; } + else + { + if(e2 < e3) + smallest = e2; + else + smallest = e3; + } + if(e1 > e2) + { + if(e1 > e3) + largest = e1; + else + largest = e3; + } + else + { + if(e2 > e3) + largest = e2; + else + largest = e3; + } + + const double ratio = smallest / largest; + // threshold is opposite + if(ratio < (1 - threshold)) + return true; return false; } @@ -389,30 +446,31 @@ bool is_needle_triangle_face(typename boost::graph_traits::face_de const TriangleMesh& tm, const double threshold) { - is_needle_triangle_face(f, tm, threshold, parameters::all_default()); + return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); } /// \ingroup PMP_repairing_grp -/// checks whether a triangle face is cap-like. -/// A cap-like triangle has an angle very close to 180 degrees. +/// checks whether a triangle face is a cap. +/// A triangle is a cap if it has an angle very close to 180 degrees. /// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param f the face to check whether is almost degenerate -/// @param tm the triangulated surface mesh upon evaluation +/// @param tm triangle mesh containing f /// @param threshold a number in the range [0, 1] to indicate the tolerance /// upon which to characterize the degeneracy. 1 means that cap triangles -/// are considered those whose vertices for an angle of 180 degrees, while 0 means that +/// are considered those whose vertices form an angle of 180 degrees, while 0 means that /// all triangles are considered caps. /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin /// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. /// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3` /// \cgalParamEnd /// \cgalNamedParamsEnd /// @@ -431,28 +489,27 @@ bool is_cap_triangle_face(typename boost::graph_traits::face_descr typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type FT; + typedef typename GetGeomTraits::type Traits; + typedef typename Traits::FT FT; typedef boost::graph_traits GT; typedef typename GT::halfedge_descriptor halfedge_descriptor; typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::value_type Point; - typedef typename Kernel_traits::Kernel::Vector_3 Vector; + typedef typename boost::property_traits::value_type Point_type; + typedef typename Kernel_traits::Kernel::Vector_3 Vector; BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) { vertex_descriptor v0 = source(h, tm); vertex_descriptor v1 = target(h, tm); vertex_descriptor v2 = target(next(h, tm), tm); - - Vector a = get(vpmap, v0) - get(vpmap, v1); + Vector a = get(vpmap, v0) - get (vpmap, v1); Vector b = get(vpmap, v2) - get(vpmap, v1); - double ab = a*b; - double aa = a.squared_length(); - double bb = b.squared_length(); - double dot_ab = a*b / (CGAL::sqrt(aa) * CGAL::sqrt(bb)); + FT aa = a.squared_length(); + FT bb = b.squared_length(); + FT dot_ab = (a*b) / (aa * bb); // threshold = 1 means no tolerance, totally degenerate - // take the opposite, because cos it -1 at 180 degrees + // take the opposite, because cos is -1 at 180 degrees if(dot_ab < -threshold) return true; } @@ -464,7 +521,7 @@ bool is_cap_triangle_face(typename boost::graph_traits::face_descr const TriangleMesh& tm, const double threshold) { - is_cap_triangle_face(f, tm, threshold, parameters::all_default()); + return is_cap_triangle_face(f, tm, threshold, parameters::all_default()); } // this function remove a border edge even if it does not satisfy the link condition. @@ -1013,7 +1070,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, // Then, remove triangles made of 3 collinear points std::set degenerate_face_set; BOOST_FOREACH(face_descriptor fd, faces(tmesh)) - if ( is_degenerate_triangle_face(fd, tmesh) ) + if ( is_degenerate_triangle_face(fd, tmesh, np)) degenerate_face_set.insert(fd); nb_deg_faces+=degenerate_face_set.size(); @@ -1044,7 +1101,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, degenerate_face_set.erase( face(hd2, tmesh) ); // remove the central vertex and check if the new face is degenerated hd=CGAL::Euler::remove_center_vertex(hd, tmesh); - if (is_degenerate_triangle_face(face(hd, tmesh), tmesh)) + if (is_degenerate_triangle_face(face(hd, tmesh), tmesh, np)) { degenerate_face_set.insert( face(hd, tmesh) ); } @@ -1572,7 +1629,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh) /// \ingroup PMP_repairing_grp /// duplicates all non-manifold vertices of the input mesh. /// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam TriangleMesh a model of `HalfedgeListGraph` and `MutableHalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param tm the triangulated surface mesh to be repaired diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/data/non_manifold_vertex_duplicated.off b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/non_manifold_vertex_duplicated.off new file mode 100644 index 00000000000..5733b99f480 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/data/non_manifold_vertex_duplicated.off @@ -0,0 +1,20 @@ +OFF +8 8 0 +0 1 0 +1 0 0 +0 0 0 +0 0 1 +2 1 0 +2 0 0 +2 0 -1 +1 0 0 +3 0 1 2 +3 2 3 0 +3 1 3 2 +3 0 3 1 +3 7 5 4 +3 7 6 5 +3 4 6 7 +3 5 6 4 + + diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 82c275c9702..e3e737e1a63 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include @@ -39,9 +39,12 @@ void check_edge_degeneracy(const char* fname) std::cerr << fname << " is not a valid off file.\n"; exit(1); } + typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + std::vector all_edges(edges(mesh).begin(), edges(mesh).end()); - BOOST_FOREACH(typename boost::graph_traits::edge_descriptor e, edges(mesh)) - CGAL::Polygon_mesh_processing::is_degenerate_edge(e, mesh); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[0], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[1], mesh)); + CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[2], mesh)); } void check_triangle_face_degeneracy(const char* fname) @@ -54,80 +57,96 @@ void check_triangle_face_degeneracy(const char* fname) exit(1); } - BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) - CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, mesh); + typedef typename boost::graph_traits::face_descriptor face_descriptor; + std::vector all_faces(faces(mesh).begin(), faces(mesh).end()); + CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[0], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[1], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[2], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); } -void test_vetices_duplication(const char* fname) +void test_vertices_merge_and_duplication(const char* fname) { std::ifstream input(fname); - Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; exit(1); } + const std::size_t initial_vertices = vertices(mesh).size(); + + // create non-manifold vertex + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); + CGAL::Polygon_mesh_processing::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); + + const std::size_t vertices_after_merge = vertices(mesh).size(); + CGAL_assertion(vertices_after_merge == initial_vertices - 1); CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); + const std::size_t final_vertices = vertices(mesh).size(); + CGAL_assertion(final_vertices == vertices_after_merge + 1); + CGAL_assertion(final_vertices == initial_vertices); } void test_vertex_non_manifoldness(const char* fname) { std::ifstream input(fname); - Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; exit(1); } - BOOST_FOREACH(typename boost::graph_traits::vertex_descriptor v, vertices(mesh)) - { - if(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)) - std::cout << "true\n"; + // create non-manifold vertex + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); + CGAL::Polygon_mesh_processing::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); + std::vector vertices_with_non_manifold(vertices(mesh).begin(), vertices(mesh).end()); + CGAL_assertion(vertices_with_non_manifold.size() == all_vertices.size() - 1); + BOOST_FOREACH(std::size_t iv, vertices(mesh)) + { + vertex_descriptor v = vertices_with_non_manifold[iv]; + if(iv == 1) + CGAL_assertion(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); + else + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); } } void test_needle(const char* fname) { std::ifstream input(fname); - Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; exit(1); } - double threshold = 0.8; - + const double threshold = 0.8; BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) { - if(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)) - std::cout << "needle\n"; + CGAL_assertion(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)); } } void test_cap(const char* fname) { std::ifstream input(fname); - Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; exit(1); } - double threshold = 0.8; - + const double threshold = 0.8; BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) { - if(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)) - std::cout << "cap\n"; + CGAL_assertion(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)); } } - int main() { fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); @@ -139,8 +158,8 @@ int main() fix("data_degeneracies/trihole.off"); check_edge_degeneracy("data_degeneracies/degtri_edge.off"); check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); - test_vetices_duplication("data_degeneracies/degtri_four.off"); - test_vertex_non_manifoldness("data/non_manifold_vertex.off"); + test_vertices_merge_and_duplication("data_degeneracies/non_manifold_vertex_duplicated.off"); + test_vertex_non_manifoldness("data_degeneracies/non_manifold_vertex_duplicated.off"); test_needle("data_degeneracies/needle.off"); test_cap("data_degeneracies/cap.off"); From 63f49b7fcc45456c222e0ba6944b265dd2e0228c Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 17 Apr 2018 11:14:41 +0200 Subject: [PATCH 14/65] move predicates to helper.h and seperate test file --- .../CGAL/Polygon_mesh_processing/helpers.h | 407 ++++++++++++++++++ .../CGAL/Polygon_mesh_processing/repair.h | 344 +-------------- .../Polygon_mesh_processing/CMakeLists.txt | 1 + .../remove_degeneracies_test.cpp | 123 ------ .../test_predicates.cpp | 139 ++++++ 5 files changed, 550 insertions(+), 464 deletions(-) create mode 100644 Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h new file mode 100644 index 00000000000..20c35e54d13 --- /dev/null +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -0,0 +1,407 @@ +// Copyright (c) 2015 GeometryFactory (France). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// You can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Licensees holding a valid commercial license may use this file in +// accordance with the commercial license agreement provided with the software. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: GPL-3.0+ +// +// +// Author(s) : Konstantinos Katrioplas + +#ifndef CGAL_POLYGON_MESH_PROCESSING_HELPERS_H +#define CGAL_POLYGON_MESH_PROCESSING_HELPERS_H + +#include +#include + + +namespace CGAL { + +namespace Polygon_mesh_processing { + +namespace internal { + +template +void merge_identical_points(PolygonMesh& mesh, + typename boost::graph_traits::vertex_descriptor v_keep, + typename boost::graph_traits::vertex_descriptor v_rm) +{ + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + halfedge_descriptor h = halfedge(v_rm, mesh); + halfedge_descriptor start = h; + + do{ + set_target(h, v_keep, mesh); + h = opposite(next(h, mesh), mesh); + } while( h != start ); + + remove_vertex(v_rm, mesh); +} +} // end internal + + + +/// \ingroup PMP_repairing_grp +/// checks whether a vertex is non-manifold. +/// +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// +/// @param v the vertex to check whether is degenerate +/// @param tm triangle mesh containing v +/// +/// \return true if the vertrex is non-manifold +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const PolygonMesh& tm) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + + boost::unordered_set halfedges_handled; + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) + halfedges_handled.insert(h); + + BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) + { + if(v == target(h, tm)) + { + if(halfedges_handled.count(h) == 0) + return true; + } + } + return false; +} + +/// \ingroup PMP_repairing_grp +/// checks whether an edge is degenerate. +/// An edge is considered degenerate if the points of its vertices are identical. +/// +/// @tparam PolygonMesh a model of `HalfedgeGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param e the edge to check whether is degenerate +/// @param pm polygon mesh containing e +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor : +/// - `Equal_3` to check whether 2 points are identical +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the edge is degenerate +template +bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, + const PolygonMesh& pm, + const NamedParameters& np) +{ + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, pm)); + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + if ( traits.equal_3_object()(get(vpmap, target(e, pm)), get(vpmap, source(e, pm))) ) + return true; + return false; +} + +template +bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, + const PolygonMesh& pm) +{ + return is_degenerate_edge(e, pm, parameters::all_default()); +} + +/// \cond SKIP_IN_DOC +template +bool is_degenerate_triangle_face( + typename boost::graph_traits::halfedge_descriptor hd, + TriangleMesh& tmesh, + const VertexPointMap& vpmap, + const Traits& traits) +{ + CGAL_assertion(!is_border(hd, tmesh)); + + const typename Traits::Point_3& p1 = get(vpmap, target( hd, tmesh) ); + const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tmesh), tmesh) ); + const typename Traits::Point_3& p3 = get(vpmap, source( hd, tmesh) ); + return traits.collinear_3_object()(p1, p2, p3); +} + +template +bool is_degenerate_triangle_face( + typename boost::graph_traits::face_descriptor fd, + TriangleMesh& tmesh, + const VertexPointMap& vpmap, + const Traits& traits) +{ + return is_degenerate_triangle_face(halfedge(fd,tmesh), tmesh, vpmap, traits); +} +/// \endcond + +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is degenerate. +/// A triangle face is degenerate if its points are collinear. +/// +/// @tparam TriangleMesh a model of `FaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the face to check whether is degenerate +/// @param tm triangle mesh containing f +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor : +/// - `Collinear_3` to check whether 3 points are collinear +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is degenerate +template +bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + typename boost::graph_traits::halfedge_descriptor hd = halfedge(f,tm); + const typename Traits::Point_3& p1 = get(vpmap, target( hd, tm) ); + const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tm), tm) ); + const typename Traits::Point_3& p3 = get(vpmap, source( hd, tm) ); + return traits.collinear_3_object()(p1, p2, p3); + +} + +template +bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm) +{ + return CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, tm, parameters::all_default()); +} + +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is needle. +/// A triangle is needle if its longest edge is much longer than the shortest one. +/// +/// @tparam TriangleMesh a model of `FaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f a face to check whether is almost degenerate +/// @param tm triangle mesh containing f +/// @param threshold a number in the range [0, 1] to indicate the tolerance +/// upon which to characterize the degeneracy. 1 means that needle triangles +/// are those that have a infinitely small edge, while 0 means that all +/// triangles are considered needles. +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is almost degenerate +template +bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; + typedef typename Traits::FT FT; + typedef boost::graph_traits GT; + typedef typename GT::vertex_descriptor vertex_descriptor; + typedef typename boost::property_traits::reference Point_ref; + + vertex_descriptor v0 = target(halfedge(f, tm), tm); + vertex_descriptor v1 = target(next(halfedge(f, tm), tm), tm); + vertex_descriptor v2 = target(next(next(halfedge(f, tm), tm), tm), tm); + Point_ref p0 = get(vpmap, v0); + Point_ref p1 = get(vpmap, v1); + Point_ref p2 = get(vpmap, v2); + + // e1 = p0p1 e2 = p1p2 e3 = p2p3 + FT e1 = CGAL::squared_distance(p0,p1); + FT e2 = CGAL::squared_distance(p1,p2); + FT e3 = CGAL::squared_distance(p2,p0); + + FT smallest, largest; + if(e1 < e2) + { + if(e1 < e3) + smallest = e1; + else + smallest = e3; + } + else + { + if(e2 < e3) + smallest = e2; + else + smallest = e3; + } + if(e1 > e2) + { + if(e1 > e3) + largest = e1; + else + largest = e3; + } + else + { + if(e2 > e3) + largest = e2; + else + largest = e3; + } + + const double ratio = smallest / largest; + // threshold is opposite + if(ratio < (1 - threshold)) + return true; + return false; +} + +template +bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) +{ + return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); +} + +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is a cap. +/// A triangle is a cap if it has an angle very close to 180 degrees. +/// +/// @tparam TriangleMesh a model of `FaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the face to check whether is almost degenerate +/// @param tm triangle mesh containing f +/// @param threshold a number in the range [0, 1] to indicate the tolerance +/// upon which to characterize the degeneracy. 1 means that cap triangles +/// are considered those whose vertices form an angle of 180 degrees, while 0 means that +/// all triangles are considered caps. +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3` +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is almost degenerate +template +bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; + typedef typename Traits::FT FT; + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GT::vertex_descriptor vertex_descriptor; + typedef typename boost::property_traits::value_type Point_type; + typedef typename Kernel_traits::Kernel::Vector_3 Vector; + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + { + vertex_descriptor v0 = source(h, tm); + vertex_descriptor v1 = target(h, tm); + vertex_descriptor v2 = target(next(h, tm), tm); + Vector a = get(vpmap, v0) - get (vpmap, v1); + Vector b = get(vpmap, v2) - get(vpmap, v1); + FT aa = a.squared_length(); + FT bb = b.squared_length(); + FT dot_ab = (a*b) / (aa * bb); + + // threshold = 1 means no tolerance, totally degenerate + // take the opposite, because cos is -1 at 180 degrees + if(dot_ab < -threshold) + return true; + } + return false; +} + +template +bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) +{ + return is_cap_triangle_face(f, tm, threshold, parameters::all_default()); +} + + + + +} } // end namespaces CGAL and PMP + + + +#endif // CGAL_POLYGON_MESH_PROCESSING_HELPERS_H + diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 27ea6f5d191..8352c191f47 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -40,6 +40,7 @@ #include #include +#include #include #include @@ -157,6 +158,7 @@ struct Less_vertex_point{ } }; +// to be removed template OutputIterator degenerate_faces(const TriangleMesh& tm, @@ -167,7 +169,7 @@ degenerate_faces(const TriangleMesh& tm, typedef typename boost::graph_traits::face_descriptor face_descriptor; BOOST_FOREACH(face_descriptor fd, faces(tm)) { - if ( is_degenerate_triangle_face(fd, tm, vpmap, traits) ) + if ( is_degenerate_triangle_face(fd, tm) ) *out++=fd; } return out; @@ -184,346 +186,6 @@ degenerate_faces(const TriangleMesh& tm, OutputIterator out) return degenerate_faces(tm, get(vertex_point, tm), Kernel(), out); } -/// \ingroup PMP_repairing_grp -/// checks whether a vertex is non-manifold. -/// -/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` -/// -/// @param v the vertex to check whether is degenerate -/// @param tm triangle mesh containing v -/// -/// \return true if the vertrex is non-manifold -template -bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, - const PolygonMesh& tm) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; - - boost::unordered_set halfedges_handled; - - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) - halfedges_handled.insert(h); - - BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) - { - if(v == target(h, tm)) - { - if(halfedges_handled.count(h) == 0) - return true; - } - } - return false; -} - -/// \ingroup PMP_repairing_grp -/// checks whether an edge is degenerate. -/// An edge is considered degenerate if the points of its vertices are identical. -/// -/// @tparam PolygonMesh a model of `HalfedgeGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param e the edge to check whether is degenerate -/// @param pm polygon mesh containing e -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`, -/// and the nested functor : -/// - `Equal_3` to check whether 2 points are identical -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \return true if the edge is degenerate -template -bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, - const PolygonMesh& pm, - const NamedParameters& np) -{ - using boost::get_param; - using boost::choose_param; - - typedef typename GetVertexPointMap::const_type VertexPointMap; - VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, pm)); - typedef typename GetGeomTraits::type Traits; - Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); - - if ( traits.equal_3_object()(get(vpmap, target(e, pm)), get(vpmap, source(e, pm))) ) - return true; - return false; -} - -template -bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, - const PolygonMesh& pm) -{ - return is_degenerate_edge(e, pm, parameters::all_default()); -} - -/// \cond SKIP_IN_DOC -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::halfedge_descriptor hd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - CGAL_assertion(!is_border(hd, tmesh)); - - const typename Traits::Point_3& p1 = get(vpmap, target( hd, tmesh) ); - const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tmesh), tmesh) ); - const typename Traits::Point_3& p3 = get(vpmap, source( hd, tmesh) ); - return traits.collinear_3_object()(p1, p2, p3); -} - -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::face_descriptor fd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - return is_degenerate_triangle_face(halfedge(fd,tmesh), tmesh, vpmap, traits); -} -/// \endcond - -/// \ingroup PMP_repairing_grp -/// checks whether a triangle face is degenerate. -/// A triangle face is degenerate if its points are collinear. -/// -/// @tparam TriangleMesh a model of `FaceGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param f the face to check whether is degenerate -/// @param tm triangle mesh containing f -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`, -/// and the nested functor : -/// - `Collinear_3` to check whether 3 points are collinear -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \return true if the triangle face is degenerate -template -bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const NamedParameters& np) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - using boost::get_param; - using boost::choose_param; - - typedef typename GetVertexPointMap::const_type VertexPointMap; - VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); - - // call from BGL helpers - return is_degenerate_triangle_face(f, tm, vpmap, traits); -} - -template -bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm) -{ - return CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, tm, parameters::all_default()); -} - -/// \ingroup PMP_repairing_grp -/// checks whether a triangle face is needle. -/// A triangle is needle if its longest edge is much longer than the shortest one. -/// -/// @tparam TriangleMesh a model of `FaceGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param f a face to check whether is almost degenerate -/// @param tm triangle mesh containing f -/// @param threshold a number in the range [0, 1] to indicate the tolerance -/// upon which to characterize the degeneracy. 1 means that needle triangles -/// are those that have a infinitely small edge, while 0 means that all -/// triangles are considered needles. -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`. -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \return true if the triangle face is almost degenerate -template -bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold, - const NamedParameters& np) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - using boost::get_param; - using boost::choose_param; - - typedef typename GetVertexPointMap::const_type VertexPointMap; - VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - typedef typename Traits::FT FT; - typedef boost::graph_traits GT; - typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::reference Point_ref; - - vertex_descriptor v0 = target(halfedge(f, tm), tm); - vertex_descriptor v1 = target(next(halfedge(f, tm), tm), tm); - vertex_descriptor v2 = target(next(next(halfedge(f, tm), tm), tm), tm); - Point_ref p0 = get(vpmap, v0); - Point_ref p1 = get(vpmap, v1); - Point_ref p2 = get(vpmap, v2); - - // e1 = p0p1 e2 = p1p2 e3 = p2p3 - FT e1 = CGAL::squared_distance(p0,p1); - FT e2 = CGAL::squared_distance(p1,p2); - FT e3 = CGAL::squared_distance(p2,p0); - - FT smallest, largest; - if(e1 < e2) - { - if(e1 < e3) - smallest = e1; - else - smallest = e3; - } - else - { - if(e2 < e3) - smallest = e2; - else - smallest = e3; - } - if(e1 > e2) - { - if(e1 > e3) - largest = e1; - else - largest = e3; - } - else - { - if(e2 > e3) - largest = e2; - else - largest = e3; - } - - const double ratio = smallest / largest; - // threshold is opposite - if(ratio < (1 - threshold)) - return true; - return false; -} - -template -bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold) -{ - return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); -} - -/// \ingroup PMP_repairing_grp -/// checks whether a triangle face is a cap. -/// A triangle is a cap if it has an angle very close to 180 degrees. -/// -/// @tparam TriangleMesh a model of `FaceGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param f the face to check whether is almost degenerate -/// @param tm triangle mesh containing f -/// @param threshold a number in the range [0, 1] to indicate the tolerance -/// upon which to characterize the degeneracy. 1 means that cap triangles -/// are considered those whose vertices form an angle of 180 degrees, while 0 means that -/// all triangles are considered caps. -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3` -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \return true if the triangle face is almost degenerate -template -bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold, - const NamedParameters& np) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - using boost::get_param; - using boost::choose_param; - - typedef typename GetVertexPointMap::const_type VertexPointMap; - VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - typedef typename Traits::FT FT; - typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; - typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::value_type Point_type; - typedef typename Kernel_traits::Kernel::Vector_3 Vector; - - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) - { - vertex_descriptor v0 = source(h, tm); - vertex_descriptor v1 = target(h, tm); - vertex_descriptor v2 = target(next(h, tm), tm); - Vector a = get(vpmap, v0) - get (vpmap, v1); - Vector b = get(vpmap, v2) - get(vpmap, v1); - FT aa = a.squared_length(); - FT bb = b.squared_length(); - FT dot_ab = (a*b) / (aa * bb); - - // threshold = 1 means no tolerance, totally degenerate - // take the opposite, because cos is -1 at 180 degrees - if(dot_ab < -threshold) - return true; - } - return false; -} - -template -bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold) -{ - return is_cap_triangle_face(f, tm, threshold, parameters::all_default()); -} - // this function remove a border edge even if it does not satisfy the link condition. // The only limitation is that the length connected component of the boundary this edge // is strictly greater than 3 diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt index ee3d82c4fe8..0c26363fca7 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt @@ -102,6 +102,7 @@ endif() create_single_source_cgal_program("test_pmp_transform.cpp") create_single_source_cgal_program("remove_degeneracies_test.cpp") create_single_source_cgal_program("test_merging_border_vertices.cpp") + create_single_source_cgal_program("test_predicates.cpp") if( TBB_FOUND ) CGAL_target_use_TBB(test_pmp_distance) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index e3e737e1a63..8b6488320e1 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include @@ -30,122 +29,6 @@ void fix(const char* fname) assert( CGAL::is_valid_polygon_mesh(mesh) ); } -void check_edge_degeneracy(const char* fname) -{ - std::ifstream input(fname); - - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - typedef typename boost::graph_traits::edge_descriptor edge_descriptor; - std::vector all_edges(edges(mesh).begin(), edges(mesh).end()); - - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[0], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[1], mesh)); - CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[2], mesh)); -} - -void check_triangle_face_degeneracy(const char* fname) -{ - std::ifstream input(fname); - - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - typedef typename boost::graph_traits::face_descriptor face_descriptor; - std::vector all_faces(faces(mesh).begin(), faces(mesh).end()); - CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[0], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[1], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[2], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); -} - -void test_vertices_merge_and_duplication(const char* fname) -{ - std::ifstream input(fname); - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - const std::size_t initial_vertices = vertices(mesh).size(); - - // create non-manifold vertex - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); - CGAL::Polygon_mesh_processing::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); - - const std::size_t vertices_after_merge = vertices(mesh).size(); - CGAL_assertion(vertices_after_merge == initial_vertices - 1); - - CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); - const std::size_t final_vertices = vertices(mesh).size(); - CGAL_assertion(final_vertices == vertices_after_merge + 1); - CGAL_assertion(final_vertices == initial_vertices); -} - -void test_vertex_non_manifoldness(const char* fname) -{ - std::ifstream input(fname); - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - // create non-manifold vertex - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); - CGAL::Polygon_mesh_processing::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); - std::vector vertices_with_non_manifold(vertices(mesh).begin(), vertices(mesh).end()); - CGAL_assertion(vertices_with_non_manifold.size() == all_vertices.size() - 1); - - BOOST_FOREACH(std::size_t iv, vertices(mesh)) - { - vertex_descriptor v = vertices_with_non_manifold[iv]; - if(iv == 1) - CGAL_assertion(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); - else - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); - } -} - -void test_needle(const char* fname) -{ - std::ifstream input(fname); - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - const double threshold = 0.8; - BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) - { - CGAL_assertion(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)); - } -} - -void test_cap(const char* fname) -{ - std::ifstream input(fname); - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - const double threshold = 0.8; - BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) - { - CGAL_assertion(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)); - } -} int main() { @@ -156,12 +39,6 @@ int main() fix("data_degeneracies/degtri_three.off"); fix("data_degeneracies/degtri_single.off"); fix("data_degeneracies/trihole.off"); - check_edge_degeneracy("data_degeneracies/degtri_edge.off"); - check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); - test_vertices_merge_and_duplication("data_degeneracies/non_manifold_vertex_duplicated.off"); - test_vertex_non_manifoldness("data_degeneracies/non_manifold_vertex_duplicated.off"); - test_needle("data_degeneracies/needle.off"); - test_cap("data_degeneracies/cap.off"); return 0; } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp new file mode 100644 index 00000000000..9d1ed0dea26 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Surface_mesh Surface_mesh; + +void check_edge_degeneracy(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + std::vector all_edges(edges(mesh).begin(), edges(mesh).end()); + + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[0], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[1], mesh)); + CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[2], mesh)); +} + +void check_triangle_face_degeneracy(const char* fname) +{ + std::ifstream input(fname); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + typedef typename boost::graph_traits::face_descriptor face_descriptor; + std::vector all_faces(faces(mesh).begin(), faces(mesh).end()); + CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[0], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[1], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[2], mesh)); + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); +} + +// temp left here: tests repair.h +void test_vertices_merge_and_duplication(const char* fname) +{ + std::ifstream input(fname); + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + const std::size_t initial_vertices = vertices(mesh).size(); + + // create non-manifold vertex + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); + CGAL::Polygon_mesh_processing::internal::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); + + const std::size_t vertices_after_merge = vertices(mesh).size(); + CGAL_assertion(vertices_after_merge == initial_vertices - 1); + + CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); + const std::size_t final_vertices = vertices(mesh).size(); + CGAL_assertion(final_vertices == vertices_after_merge + 1); + CGAL_assertion(final_vertices == initial_vertices); +} + +void test_vertex_non_manifoldness(const char* fname) +{ + std::ifstream input(fname); + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + // create non-manifold vertex + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); + CGAL::Polygon_mesh_processing::internal::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); + std::vector vertices_with_non_manifold(vertices(mesh).begin(), vertices(mesh).end()); + CGAL_assertion(vertices_with_non_manifold.size() == all_vertices.size() - 1); + + BOOST_FOREACH(std::size_t iv, vertices(mesh)) + { + vertex_descriptor v = vertices_with_non_manifold[iv]; + if(iv == 1) + CGAL_assertion(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); + else + CGAL_assertion(!CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); + } +} + +void test_needle(const char* fname) +{ + std::ifstream input(fname); + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + const double threshold = 0.8; + BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) + { + CGAL_assertion(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)); + } +} + +void test_cap(const char* fname) +{ + std::ifstream input(fname); + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << fname << " is not a valid off file.\n"; + exit(1); + } + + const double threshold = 0.8; + BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) + { + CGAL_assertion(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)); + } +} + +int main() +{ + check_edge_degeneracy("data_degeneracies/degtri_edge.off"); + check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); + test_vertices_merge_and_duplication("data_degeneracies/non_manifold_vertex_duplicated.off"); + test_vertex_non_manifoldness("data_degeneracies/non_manifold_vertex_duplicated.off"); + test_needle("data_degeneracies/needle.off"); + test_cap("data_degeneracies/cap.off"); + + return 0; +} From 71041e03769ae7dc749488a5d8f0778877b33469 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 17 Apr 2018 12:41:17 +0200 Subject: [PATCH 15/65] replace is_degenerate_triangle_face predicate with new version from PMP helpers --- .../CGAL/Polygon_mesh_processing/helpers.h | 27 ------------------- .../Isotropic_remeshing/remesh_impl.h | 13 ++++----- .../CGAL/Polygon_mesh_processing/repair.h | 19 ++----------- .../Plugins/PMP/Degenerated_faces_plugin.cpp | 3 ++- .../Plugins/PMP/Selection_plugin.cpp | 7 +++-- .../Edit_polyhedron_plugin.cpp | 6 ++--- .../demo/Polyhedron/Scene_polyhedron_item.cpp | 3 ++- .../Scene_polyhedron_selection_item.cpp | 3 ++- .../Polyhedron/Scene_surface_mesh_item.cpp | 3 ++- .../include/CGAL/statistics_helpers.h | 3 ++- 10 files changed, 24 insertions(+), 63 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h index 20c35e54d13..24174b778d9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -136,33 +136,6 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript return is_degenerate_edge(e, pm, parameters::all_default()); } -/// \cond SKIP_IN_DOC -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::halfedge_descriptor hd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - CGAL_assertion(!is_border(hd, tmesh)); - - const typename Traits::Point_3& p1 = get(vpmap, target( hd, tmesh) ); - const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tmesh), tmesh) ); - const typename Traits::Point_3& p3 = get(vpmap, source( hd, tmesh) ); - return traits.collinear_3_object()(p1, p2, p3); -} - -template -bool is_degenerate_triangle_face( - typename boost::graph_traits::face_descriptor fd, - TriangleMesh& tmesh, - const VertexPointMap& vpmap, - const Traits& traits) -{ - return is_degenerate_triangle_face(halfedge(fd,tmesh), tmesh, vpmap, traits); -} -/// \endcond - /// \ingroup PMP_repairing_grp /// checks whether a triangle face is degenerate. /// A triangle face is degenerate if its points are collinear. diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h index bfbe181b40e..9686f4c271b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -364,7 +365,7 @@ namespace internal { BOOST_FOREACH(face_descriptor f, face_range) { - if (is_degenerate_triangle_face(halfedge(f,mesh_),mesh_,vpmap_,GeomTraits())){ + if (is_degenerate_triangle_face(f, mesh_,)){ continue; } Patch_id pid = get_patch_id(f); @@ -1593,7 +1594,7 @@ private: { if (is_border(h, mesh_)) continue; - if (is_degenerate_triangle_face(h, mesh_, vpmap_, GeomTraits())) + if (is_degenerate_triangle_face(face(h), mesh_)) degenerate_faces.insert(h); } while(!degenerate_faces.empty()) @@ -1601,7 +1602,7 @@ private: halfedge_descriptor h = *(degenerate_faces.begin()); degenerate_faces.erase(degenerate_faces.begin()); - if (!is_degenerate_triangle_face(h, mesh_, vpmap_, GeomTraits())) + if (!is_degenerate_triangle_face(face(h), mesh_)) //this can happen when flipping h has consequences further in the mesh continue; @@ -1654,10 +1655,10 @@ private: } if (!is_border(hf, mesh_) - && is_degenerate_triangle_face(hf, mesh_, vpmap_, GeomTraits())) + && is_degenerate_triangle_face(face(h), mesh_)) degenerate_faces.insert(hf); if (!is_border(hfo, mesh_) - && is_degenerate_triangle_face(hfo, mesh_, vpmap_, GeomTraits())) + && is_degenerate_triangle_face(face(h), mesh_)) degenerate_faces.insert(hfo); break; @@ -1676,7 +1677,7 @@ private: { if (is_border(h, mesh_)) continue; - if (is_degenerate_triangle_face(h, mesh_, vpmap_, GeomTraits())) + if (is_degenerate_triangle_face(face(h), mesh_)) return true; } return false; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 8352c191f47..cced686125d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -158,13 +158,9 @@ struct Less_vertex_point{ } }; -// to be removed -template +template OutputIterator -degenerate_faces(const TriangleMesh& tm, - const VertexPointMap& vpmap, - const Traits& traits, - OutputIterator out) +degenerate_faces(const TriangleMesh& tm, OutputIterator out) { typedef typename boost::graph_traits::face_descriptor face_descriptor; BOOST_FOREACH(face_descriptor fd, faces(tm)) @@ -175,17 +171,6 @@ degenerate_faces(const TriangleMesh& tm, return out; } -template -OutputIterator -degenerate_faces(const TriangleMesh& tm, OutputIterator out) -{ - typedef typename boost::property_map::type Vpm; - typedef typename boost::property_traits::value_type Point; - typedef typename Kernel_traits::Kernel Kernel; - - return degenerate_faces(tm, get(vertex_point, tm), Kernel(), out); -} - // this function remove a border edge even if it does not satisfy the link condition. // The only limitation is that the length connected component of the boundary this edge // is strictly greater than 3 diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp index 8fd94b78149..b7c359e9ab1 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #ifdef USE_SURFACE_MESH typedef Scene_surface_mesh_item Scene_facegraph_item; #else @@ -87,7 +88,7 @@ bool isDegen(Mesh* mesh, std::vector::face_de BOOST_FOREACH(FaceDescriptor f, faces(*mesh)) { if(is_triangle(halfedge(f, *mesh), *mesh) - && is_degenerate_triangle_face(f, *mesh, get(boost::vertex_point, *mesh), Kernel()) ) + && CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, *mesh) ) out_faces.push_back(f); } return !out_faces.empty(); diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp index a6cf3a63299..ac1bfd837d4 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #ifdef USE_SURFACE_MESH @@ -745,10 +746,8 @@ public Q_SLOTS: bool is_valid = true; BOOST_FOREACH(boost::graph_traits::face_descriptor fd, faces(*selection_item->polyhedron())) { - if (CGAL::is_degenerate_triangle_face(fd, - *selection_item->polyhedron(), - vpmap, - CGAL::Kernel_traits< boost::property_traits::value_type >::Kernel())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(fd, + *selection_item->polyhedron())) { is_valid = false; break; diff --git a/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp index 730d6447313..18a5d1bb8e0 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp @@ -9,6 +9,7 @@ #include "Scene_edit_polyhedron_item.h" #include "Scene_polyhedron_selection_item.h" #include +#include #include #include #include @@ -421,10 +422,7 @@ void Polyhedron_demo_edit_polyhedron_plugin::dock_widget_visibility_changed(bool bool is_valid = true; BOOST_FOREACH(boost::graph_traits::face_descriptor fd, faces(*poly_item->face_graph())) { - if (CGAL::is_degenerate_triangle_face(fd, - *poly_item->face_graph(), - get(boost::vertex_point, - *poly_item->face_graph()), Kernel())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(fd, *poly_item->face_graph())) { is_valid = false; break; diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp index 58a466c7ad1..266f5331b94 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -276,7 +277,7 @@ void* Scene_polyhedron_item_priv::get_aabb_tree() int index =0; BOOST_FOREACH( Polyhedron::Facet_iterator f, faces(*poly)) { - if (CGAL::is_degenerate_triangle_face(f, *poly, get(CGAL::vertex_point, *poly), Kernel())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, *poly)) continue; if(!f->is_triangle()) { diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp index 6df22ab25e7..488e874d416 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp @@ -2,6 +2,7 @@ #include "Scene_polyhedron_selection_item.h" #include #include +#include #include #include #include @@ -2032,7 +2033,7 @@ bool Scene_polyhedron_selection_item_priv::canAddFace(fg_halfedge_descriptor hc, fg_halfedge_descriptor res = CGAL::Euler::add_face_to_border(t,hc, *item->polyhedron()); - if(CGAL::is_degenerate_triangle_face(res, *item->polyhedron(), get(CGAL::vertex_point, *item->polyhedron()), Kernel())) + if(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(res, *item->polyhedron())) { CGAL::Euler::remove_face(res, *item->polyhedron()); tempInstructions("Edge not selected : resulting facet is degenerated.", diff --git a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp index 29c31b34c70..75e60907f61 100644 --- a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -1039,7 +1040,7 @@ void* Scene_surface_mesh_item_priv::get_aabb_tree() BOOST_FOREACH( face_descriptor f, faces(*sm)) { //if face is degenerate, skip it - if (CGAL::is_degenerate_triangle_face(f, *sm, get(CGAL::vertex_point, *sm), EPICK())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, *sm)) continue; //if face not triangle, triangulate corresponding primitive before adding it to the tree if(!CGAL::is_triangle(halfedge(f, *sm), *sm)) diff --git a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h index 28799d40fd9..711a641dcc3 100644 --- a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h +++ b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h @@ -14,6 +14,7 @@ #include #include +#include template @@ -92,7 +93,7 @@ unsigned int nb_degenerate_faces(Mesh* poly, VPmap vpmap) unsigned int nb = 0; BOOST_FOREACH(face_descriptor f, faces(*poly)) { - if (CGAL::is_degenerate_triangle_face(f, *poly, vpmap, Traits())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, *poly)) ++nb; } return nb; From c6afed86a3db493a1bef08a65e193b8f9a9f229c Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 17 Apr 2018 13:31:51 +0200 Subject: [PATCH 16/65] use cosine for threshold on needles and caps --- .../CGAL/Polygon_mesh_processing/helpers.h | 205 ++++++++---------- .../test_predicates.cpp | 2 +- 2 files changed, 88 insertions(+), 119 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h index 24174b778d9..dad4a29831a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -50,14 +50,12 @@ void merge_identical_points(PolygonMesh& mesh, } } // end internal - - /// \ingroup PMP_repairing_grp /// checks whether a vertex is non-manifold. /// /// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` /// -/// @param v the vertex to check whether is degenerate +/// @param v the vertex /// @param tm triangle mesh containing v /// /// \return true if the vertrex is non-manifold @@ -93,7 +91,7 @@ bool is_non_manifold_vertex(typename boost::graph_traits::vertex_de /// @tparam PolygonMesh a model of `HalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param e the edge to check whether is degenerate +/// @param e the edge /// @param pm polygon mesh containing e /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// @@ -143,7 +141,7 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f the face to check whether is degenerate +/// @param f the triangle face /// @param tm triangle mesh containing f /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// @@ -198,12 +196,11 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f a face to check whether is almost degenerate +/// @param f the triangle face /// @param tm triangle mesh containing f -/// @param threshold a number in the range [0, 1] to indicate the tolerance -/// upon which to characterize the degeneracy. 1 means that needle triangles -/// are those that have a infinitely small edge, while 0 means that all -/// triangles are considered needles. +/// @param threshold the cosine of an angle of f. +/// The threshold is in range [0 1] and corresponds to +/// angles between 0 and 90 degrees. /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -216,7 +213,7 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the triangle face is almost degenerate +/// \return true if the triangle face is a needle template bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, const TriangleMesh& tm, @@ -224,109 +221,8 @@ bool is_needle_triangle_face(typename boost::graph_traits::face_de const NamedParameters& np) { CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - using boost::get_param; - using boost::choose_param; - - typedef typename GetVertexPointMap::const_type VertexPointMap; - VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - typedef typename Traits::FT FT; - typedef boost::graph_traits GT; - typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::reference Point_ref; - - vertex_descriptor v0 = target(halfedge(f, tm), tm); - vertex_descriptor v1 = target(next(halfedge(f, tm), tm), tm); - vertex_descriptor v2 = target(next(next(halfedge(f, tm), tm), tm), tm); - Point_ref p0 = get(vpmap, v0); - Point_ref p1 = get(vpmap, v1); - Point_ref p2 = get(vpmap, v2); - - // e1 = p0p1 e2 = p1p2 e3 = p2p3 - FT e1 = CGAL::squared_distance(p0,p1); - FT e2 = CGAL::squared_distance(p1,p2); - FT e3 = CGAL::squared_distance(p2,p0); - - FT smallest, largest; - if(e1 < e2) - { - if(e1 < e3) - smallest = e1; - else - smallest = e3; - } - else - { - if(e2 < e3) - smallest = e2; - else - smallest = e3; - } - if(e1 > e2) - { - if(e1 > e3) - largest = e1; - else - largest = e3; - } - else - { - if(e2 > e3) - largest = e2; - else - largest = e3; - } - - const double ratio = smallest / largest; - // threshold is opposite - if(ratio < (1 - threshold)) - return true; - return false; -} - -template -bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold) -{ - return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); -} - -/// \ingroup PMP_repairing_grp -/// checks whether a triangle face is a cap. -/// A triangle is a cap if it has an angle very close to 180 degrees. -/// -/// @tparam TriangleMesh a model of `FaceGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param f the face to check whether is almost degenerate -/// @param tm triangle mesh containing f -/// @param threshold a number in the range [0, 1] to indicate the tolerance -/// upon which to characterize the degeneracy. 1 means that cap triangles -/// are considered those whose vertices form an angle of 180 degrees, while 0 means that -/// all triangles are considered caps. -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3` -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \return true if the triangle face is almost degenerate -template -bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold, - const NamedParameters& np) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); + CGAL_assertion(threshold >= 0); + CGAL_assertion(threshold <= 1); using boost::get_param; using boost::choose_param; @@ -351,11 +247,84 @@ bool is_cap_triangle_face(typename boost::graph_traits::face_descr Vector b = get(vpmap, v2) - get(vpmap, v1); FT aa = a.squared_length(); FT bb = b.squared_length(); - FT dot_ab = (a*b) / (aa * bb); + FT squared_dot_ab = ((a*b)*(a*b)) / (aa * bb); - // threshold = 1 means no tolerance, totally degenerate - // take the opposite, because cos is -1 at 180 degrees - if(dot_ab < -threshold) + if(squared_dot_ab > threshold * threshold) + return true; + } + return false; + +} + +template +bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) +{ + return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); +} + +/// \ingroup PMP_repairing_grp +/// checks whether a triangle face is a cap. +/// A triangle is a cap if it has an angle very close to 180 degrees. +/// +/// @tparam TriangleMesh a model of `FaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param f the triangle face +/// @param tm triangle mesh containing f +/// @param threshold the cosine of an angle of f. +/// The threshold is in range [-1 0] and corresponds to +/// angles between 90 and 180 degrees. +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3` +/// \cgalParamEnd +/// \cgalNamedParamsEnd +/// +/// \return true if the triangle face is a cap +template +bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + CGAL_assertion(threshold >= -1); + CGAL_assertion(threshold <= 0); + + using boost::get_param; + using boost::choose_param; + + typedef typename GetVertexPointMap::const_type VertexPointMap; + VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), + get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; + typedef typename Traits::FT FT; + typedef boost::graph_traits GT; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GT::vertex_descriptor vertex_descriptor; + typedef typename boost::property_traits::value_type Point_type; + typedef typename Kernel_traits::Kernel::Vector_3 Vector; + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + { + vertex_descriptor v0 = source(h, tm); + vertex_descriptor v1 = target(h, tm); + vertex_descriptor v2 = target(next(h, tm), tm); + Vector a = get(vpmap, v0) - get (vpmap, v1); + Vector b = get(vpmap, v2) - get(vpmap, v1); + FT aa = a.squared_length(); + FT bb = b.squared_length(); + FT squared_dot_ab = ((a*b)*(a*b)) / (aa * bb); + + if(squared_dot_ab > threshold * threshold) return true; } return false; diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp index 9d1ed0dea26..679f96762d3 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp @@ -119,7 +119,7 @@ void test_cap(const char* fname) exit(1); } - const double threshold = 0.8; + const double threshold = -0.8; BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) { CGAL_assertion(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)); From 032ee2828a29dbc3fa7f4f427e2f35ae10266724 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Wed, 18 Apr 2018 18:11:47 +0200 Subject: [PATCH 17/65] named parameters for duplicate non-manifold vertices --- .../CGAL/boost/graph/parameters_interface.h | 1 + BGL/test/BGL/test_cgal_bgl_named_params.cpp | 3 ++ .../CGAL/Polygon_mesh_processing/helpers.h | 51 +++++++++++++++++++ .../CGAL/Polygon_mesh_processing/repair.h | 49 ++++++++++++++---- .../test_predicates.cpp | 13 +++-- 5 files changed, 102 insertions(+), 15 deletions(-) diff --git a/BGL/include/CGAL/boost/graph/parameters_interface.h b/BGL/include/CGAL/boost/graph/parameters_interface.h index 16357cb39dd..657972a74a8 100644 --- a/BGL/include/CGAL/boost/graph/parameters_interface.h +++ b/BGL/include/CGAL/boost/graph/parameters_interface.h @@ -70,6 +70,7 @@ CGAL_add_named_parameter(projection_functor_t, projection_functor, projection_fu CGAL_add_named_parameter(throw_on_self_intersection_t, throw_on_self_intersection, throw_on_self_intersection) CGAL_add_named_parameter(clip_volume_t, clip_volume, clip_volume) CGAL_add_named_parameter(use_compact_clipper_t, use_compact_clipper, use_compact_clipper) +CGAL_add_named_parameter(output_iterator_t, output_iterator, output_iterator) // List of named parameters that we use in the package 'Surface Mesh Simplification' CGAL_add_named_parameter(get_cost_policy_t, get_cost_policy, get_cost) diff --git a/BGL/test/BGL/test_cgal_bgl_named_params.cpp b/BGL/test/BGL/test_cgal_bgl_named_params.cpp index 4bb47789fde..c0081450d1e 100644 --- a/BGL/test/BGL/test_cgal_bgl_named_params.cpp +++ b/BGL/test/BGL/test_cgal_bgl_named_params.cpp @@ -92,6 +92,7 @@ void test(const NamedParameters& np) assert(get_param(np, CGAL::internal_np::verbosity_level).v == 41); assert(get_param(np, CGAL::internal_np::projection_functor).v == 42); assert(get_param(np, CGAL::internal_np::apply_per_connected_component).v == 46); + assert(get_param(np, CGAL::internal_np::output_iterator).v == 47); // Test types @@ -162,6 +163,7 @@ void test(const NamedParameters& np) check_same_type<41>(get_param(np, CGAL::internal_np::verbosity_level)); check_same_type<42>(get_param(np, CGAL::internal_np::projection_functor)); check_same_type<46>(get_param(np, CGAL::internal_np::apply_per_connected_component)); + check_same_type472>(get_param(np, CGAL::internal_np::output_iterator)); } int main() @@ -217,6 +219,7 @@ int main() .clip_volume(A<44>(44)) .use_compact_clipper(A<45>(45)) .apply_per_connected_component(A<46>(46)) + .output_iterator(A<47>(47)) ); return EXIT_SUCCESS; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h index dad4a29831a..ea38a91b835 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -32,6 +32,57 @@ namespace Polygon_mesh_processing { namespace internal { +template +struct No_constraint_pmap +{ +public: + typedef Descriptor key_type; + typedef bool value_type; + typedef value_type& reference; + typedef boost::read_write_property_map_tag category; + + friend bool get(const No_constraint_pmap& , const key_type& ) { + return false; + } + friend void put(No_constraint_pmap& , const key_type& , const bool ) {} +}; + +template +struct Vertex_collector +{ + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + void collect_vertices(vertex_descriptor v1, vertex_descriptor v2) + { + std::vector& verts = collections[v1]; + if (verts.empty()) + verts.push_back(v1); + verts.push_back(v2); + } + + void dump(OutputIterator out) + { + typedef std::pair > Pair_type; + BOOST_FOREACH(const Pair_type& p, collections) + { + *out++=p.second; + } + } + + std::map > collections; +}; + +template +struct Vertex_collector +{ + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + void collect_vertices(vertex_descriptor, vertex_descriptor) + {} + + void dump(Emptyset_iterator) + {} +}; + +// used only for testing template void merge_identical_points(PolygonMesh& mesh, typename boost::graph_traits::vertex_descriptor v_keep, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index cced686125d..b5579b5204b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -1284,14 +1284,21 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh) /// /// \cgalNamedParamsBegin /// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` /// \cgalParamEnd +/// \cgalParamBegin{vertex_is_constrained_map} a writable property map with `vertex_descriptor` +/// as key and `bool` as `value_type`. `put(pmap, v, true)` will be called for each duplicated +/// vertices and the input one. +/// \cgalParamEnd +/// \cgalParamBegin{output_iterator} an output iterator where `std::vector` can be put. +/// The first vertex of the vector is an input vertex that was non-manifold, +/// the other vertices in the vertex are the new vertices created to fix +/// the non-manifoldness. +/// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the triangle face is degenerate +/// \return the number of vertices created template std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, const NamedParameters& np) @@ -1301,14 +1308,33 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, using boost::get_param; using boost::choose_param; - typedef typename GetVertexPointMap::type VertexPointMap; - VertexPointMap vpm = choose_param(get_param(np, internal_np::vertex_point), - get_property_map(vertex_point, tm)); - typedef boost::graph_traits GT; typedef typename GT::vertex_descriptor vertex_descriptor; typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GetVertexPointMap::type VertexPointMap; + VertexPointMap vpm = choose_param(get_param(np, internal_np::vertex_point), + get_property_map(vertex_point, tm)); + + typedef typename boost::lookup_named_param_def < + internal_np::vertex_is_constrained_t, + NamedParameters, + internal::No_constraint_pmap//default + > ::type VerticesMap; + VerticesMap cmap + = choose_param(get_param(np, internal_np::vertex_is_constrained), + internal::No_constraint_pmap()); + + typedef typename boost::lookup_named_param_def < + internal_np::output_iterator_t, + NamedParameters, + Emptyset_iterator + > ::type Output_iterator; + Output_iterator out + = choose_param(get_param(np, internal_np::output_iterator), + Emptyset_iterator()); + + internal::Vertex_collector dmap; boost::unordered_set vertices_handled; boost::unordered_set halfedges_handled; @@ -1322,6 +1348,7 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, vertex_descriptor vd = target(h, tm); if ( !vertices_handled.insert(vd).second ) { + put(cmap, vd, true); // store the originals non_manifold_cones.push_back(h); } else @@ -1341,6 +1368,8 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, halfedge_descriptor start = h; vertex_descriptor new_vd = add_vertex(tm); ++nb_new_vertices; + put(cmap, new_vd, true); // store the duplicates + dmap.collect_vertices(target(h, tm), new_vd); put(vpm, new_vd, get(vpm, target(h, tm))); set_halfedge(new_vd, h, tm); do{ @@ -1348,8 +1377,8 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, h=opposite(next(h, tm), tm); } while(h!=start); } + dmap.dump(out); } - return nb_new_vertices; } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp index 679f96762d3..3bf2ffcd494 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp @@ -43,7 +43,7 @@ void check_triangle_face_degeneracy(const char* fname) CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); } -// temp left here: tests repair.h +// tests repair.h void test_vertices_merge_and_duplication(const char* fname) { std::ifstream input(fname); @@ -62,10 +62,13 @@ void test_vertices_merge_and_duplication(const char* fname) const std::size_t vertices_after_merge = vertices(mesh).size(); CGAL_assertion(vertices_after_merge == initial_vertices - 1); - CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh); - const std::size_t final_vertices = vertices(mesh).size(); - CGAL_assertion(final_vertices == vertices_after_merge + 1); - CGAL_assertion(final_vertices == initial_vertices); + std::vector< std::vector > duplicated_vertices; + CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh, + CGAL::parameters::output_iterator(std::back_inserter(duplicated_vertices))); + const std::size_t final_vertices_size = vertices(mesh).size(); + CGAL_assertion(final_vertices_size == vertices_after_merge + 1); + CGAL_assertion(final_vertices_size == initial_vertices); + CGAL_assertion(duplicated_vertices.size() == 2); } void test_vertex_non_manifoldness(const char* fname) From 99db9a0aaf22e5b6774ed7e1c9d617a013cea5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 20 Apr 2018 11:50:54 +0200 Subject: [PATCH 18/65] WIP correctly linking halfedges around merged vertices ... also disable the merge between cycles as it is not straight forward it will be always possible --- .../merge_border_vertices.h | 154 +++++++++++++++--- .../test_merging_border_vertices.cpp | 7 +- 2 files changed, 132 insertions(+), 29 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 3435e5fd7bd..0dc8560cad5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -37,6 +37,7 @@ namespace Polygon_mesh_processing{ namespace internal { +#if 0 // warning: vertices will be altered (sorted) template void detect_identical_vertices(std::vector& vertices, @@ -76,11 +77,77 @@ void detect_identical_vertices(std::vector& vertices, ++i; } } +#endif + +template +struct Less_on_point_of_target +{ + typedef typename boost::graph_traits::halfedge_descriptor + halfedge_descriptor; + typedef typename boost::property_traits::reference Point; + + Less_on_point_of_target(const PM& pm, + const VertexPointMap& vpm) + : pm(pm), + vpm(vpm) + {} + + bool operator()(halfedge_descriptor h1, + halfedge_descriptor h2) const + { + return get(vpm, target(h1, pm)) < get(vpm, target(h2, pm)); + } + + const PM& pm; + const VertexPointMap& vpm; +}; + + +// warning: cycle_hedges will be altered (sorted) +template +void detect_identical_vertices(std::vector& cycle_hedges, + std::vector< std::vector >& hedges_with_identical_point_target, + const PolygonMesh& pm, + Vpm vpm) +{ + // sort vertices using their point to ease the detection + // of vertices with identical points + Less_on_point_of_target less(pm, vpm); + std::sort( cycle_hedges.begin(), cycle_hedges.end(), less); + + std::size_t nbv=cycle_hedges.size(); + std::size_t i=1; + + while(i!=nbv) + { + if ( get(vpm, target(cycle_hedges[i], pm)) == + get(vpm, target(cycle_hedges[i-1], pm)) ) + { + hedges_with_identical_point_target.push_back( std::vector() ); + hedges_with_identical_point_target.back().push_back(cycle_hedges[i-1]); + hedges_with_identical_point_target.back().push_back(cycle_hedges[i]); + while(++i!=nbv) + { + if ( get(vpm, target(cycle_hedges[i], pm)) == + get(vpm, target(cycle_hedges[i-1], pm)) ) + hedges_with_identical_point_target.back().push_back(cycle_hedges[i]); + else + { + ++i; + break; + } + } + } + else + ++i; + } +} } // end of internal /// \todo document me /// It should probably go into BGL package +/// It should make sense to also return the length of each cycle template OutputIterator extract_boundary_cycles(PolygonMesh& pm, @@ -103,33 +170,44 @@ extract_boundary_cycles(PolygonMesh& pm, /// \ingroup PMP_repairing_grp /// \todo document me -template -void merge_boundary_vertices(const VertexRange& vertices, - PolygonMesh& pm) +/// we merge the all the target of the halfedges in `hedges` +/// hedges must be sorted along the cycle +template +void merge_boundary_vertices_in_cycle(const HalfedgeRange& sorted_hedges, + PolygonMesh& pm) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - vertex_descriptor v_kept=*boost::begin(vertices); + halfedge_descriptor in_h_kept = *boost::begin(sorted_hedges); + halfedge_descriptor out_h_kept = next(in_h_kept, pm); + vertex_descriptor v_kept=target(in_h_kept, pm); + std::vector vertices_to_rm; - BOOST_FOREACH(vertex_descriptor vd, vertices) + BOOST_FOREACH(halfedge_descriptor in_h_rm, sorted_hedges) { - if (vd==v_kept) continue; // skip identical vertices + vertex_descriptor vd = target(in_h_rm, pm); + if (vd==v_kept) continue; // skip identical vertices (in particular this skips the first halfedge) if (edge(vd, v_kept, pm).second) continue; // skip null edges bool shall_continue=false; - BOOST_FOREACH(halfedge_descriptor hd, halfedges_around_target(v_kept, pm)) + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v_kept, pm)) { - if (edge(vd, source(hd, pm), pm).second) + if (edge(vd, source(h, pm), pm).second) { shall_continue=true; break; } } if (shall_continue) continue; // skip vertices already incident to the same vertex - - internal::update_target_vertex(halfedge(vd, pm), v_kept, pm); + // update the vertex of the halfedges incident to the vertex to remove + internal::update_target_vertex(in_h_rm, v_kept, pm); + // update next/prev pointers around the 2 vertices to be merged + halfedge_descriptor out_h_rm = next(in_h_rm, pm); + set_next(in_h_kept, out_h_rm, pm); + set_next(in_h_rm, out_h_kept, pm); vertices_to_rm.push_back(vd); + out_h_kept=out_h_rm; } BOOST_FOREACH(vertex_descriptor vd, vertices_to_rm) @@ -139,30 +217,54 @@ void merge_boundary_vertices(const VertexRange& vertices, /// \ingroup PMP_repairing_grp /// \todo document me template -void merge_duplicated_vertices_in_boundary_cycle(typename boost::graph_traits::halfedge_descriptor h, - PolygonMesh& pm, - const NamedParameter& np) +void merge_duplicated_vertices_in_boundary_cycle( + typename boost::graph_traits::halfedge_descriptor h, + PolygonMesh& pm, + const NamedParameter& np) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename GetVertexPointMap::const_type Vpm; Vpm vpm = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, pm)); - // collect all the vertices of the cycle - std::vector vertices; + // collect all the halfedges of the cycle + std::vector cycle_hedges; halfedge_descriptor start=h; do{ - vertices.push_back(target(h,pm)); + cycle_hedges.push_back(h); h=next(h, pm); }while(start!=h); - std::vector< std::vector > identical_vertices; - internal::detect_identical_vertices(vertices, identical_vertices, vpm); + std::vector< std::vector > hedges_with_identical_point_target; + internal::detect_identical_vertices(cycle_hedges, hedges_with_identical_point_target, pm, vpm); - BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) - merge_boundary_vertices(vrtcs, pm); + BOOST_FOREACH(const std::vector& hedges, + hedges_with_identical_point_target) + { + start=hedges.front(); + // collect all halfedges in the cycle + std::vector sorted_hedges; + h=start; + do{ + sorted_hedges.push_back(h); + do + { + h=next(h, pm); + } + while( get(vpm, target(h, pm)) != get(vpm, target(start, pm)) ); + } + while(h!=start); + + if (sorted_hedges.size() != hedges.size()) + { + std::cerr << "WARNING: cycle broken at " << get(vpm, target(start, pm)) << ". Skipped\n"; + std::cout << sorted_hedges.size() << " vs " << hedges.size() << "\n"; + CGAL_assertion(sorted_hedges.size() == hedges.size()); + continue; + } + merge_boundary_vertices_in_cycle(sorted_hedges, pm); + } } /// \ingroup PMP_repairing_grp @@ -180,7 +282,7 @@ void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, merge_duplicated_vertices_in_boundary_cycle(h, pm, np); } - +#if 0 /// \ingroup PMP_repairing_grp /// \todo document me template @@ -207,8 +309,7 @@ void merge_duplicated_boundary_vertices( PolygonMesh& pm, BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) merge_boundary_vertices(vrtcs, pm); } - - +#endif template void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) @@ -221,15 +322,16 @@ void merge_duplicated_vertices_in_boundary_cycle( typename boost::graph_traits::halfedge_descriptor h, PolygonMesh& pm) { - merge_duplicated_vertices_in_boundary_cycles(h, pm, parameters::all_default()); + merge_duplicated_vertices_in_boundary_cycle(h, pm, parameters::all_default()); } +#if 0 template void merge_duplicated_boundary_vertices(PolygonMesh& pm) { merge_duplicated_boundary_vertices(pm, parameters::all_default()); } - +#endif } } // end of CGAL::Polygon_mesh_processing diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp index 1abc453207a..a3052bbd2fd 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp @@ -38,6 +38,7 @@ void test_merge_duplicated_vertices_in_boundary_cycles(const char* fname, } } +#if 0 void test_merge_duplicated_boundary_vertices(const char* fname, std::size_t expected_nb_vertices) { @@ -64,21 +65,21 @@ void test_merge_duplicated_boundary_vertices(const char* fname, output << mesh; } } - +#endif int main(int argc, char** argv) { if (argc==1) { test_merge_duplicated_vertices_in_boundary_cycles("data/merge_points.off", 43); - test_merge_duplicated_boundary_vertices("data/merge_points.off", 40); + // test_merge_duplicated_boundary_vertices("data/merge_points.off", 40); } else { for (int i=1; i< argc; ++i) { test_merge_duplicated_vertices_in_boundary_cycles(argv[i], 0); - test_merge_duplicated_boundary_vertices(argv[i], 0); + // test_merge_duplicated_boundary_vertices(argv[i], 0); } } return 0; From ee3636d57eb86c59b3ad1c5267576dbeed5f86e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 20 Apr 2018 14:01:24 +0200 Subject: [PATCH 19/65] directly sort halfedges and use the ordering to detect illegal merges a merge is considered as illegal if it makes to vertices to be merged unreachable. For now if a cycle contain an illegal merge, all merges of the cycle are ignored. --- .../merge_border_vertices.h | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 0dc8560cad5..4a3ce2f8406 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -92,10 +92,14 @@ struct Less_on_point_of_target vpm(vpm) {} - bool operator()(halfedge_descriptor h1, - halfedge_descriptor h2) const + bool operator()(const std::pair& h1, + const std::pair& h2) const { - return get(vpm, target(h1, pm)) < get(vpm, target(h2, pm)); + if ( get(vpm, target(h1.first, pm)) < get(vpm, target(h2.first, pm)) ) + return true; + if ( get(vpm, target(h1.first, pm)) > get(vpm, target(h2.first, pm)) ) + return false; + return h1.second < h2.second; } const PM& pm; @@ -105,10 +109,11 @@ struct Less_on_point_of_target // warning: cycle_hedges will be altered (sorted) template -void detect_identical_vertices(std::vector& cycle_hedges, - std::vector< std::vector >& hedges_with_identical_point_target, - const PolygonMesh& pm, - Vpm vpm) +void detect_identical_mergeable_vertices( + std::vector< std::pair >& cycle_hedges, + std::vector< std::vector >& hedges_with_identical_point_target, + const PolygonMesh& pm, + Vpm vpm) { // sort vertices using their point to ease the detection // of vertices with identical points @@ -118,19 +123,27 @@ void detect_identical_vertices(std::vector& cycle_hedges, std::size_t nbv=cycle_hedges.size(); std::size_t i=1; + std::set< std::pair > intervals; + while(i!=nbv) { - if ( get(vpm, target(cycle_hedges[i], pm)) == - get(vpm, target(cycle_hedges[i-1], pm)) ) + if ( get(vpm, target(cycle_hedges[i].first, pm)) == + get(vpm, target(cycle_hedges[i-1].first, pm)) ) { hedges_with_identical_point_target.push_back( std::vector() ); - hedges_with_identical_point_target.back().push_back(cycle_hedges[i-1]); - hedges_with_identical_point_target.back().push_back(cycle_hedges[i]); + hedges_with_identical_point_target.back().push_back(cycle_hedges[i-1].first); + hedges_with_identical_point_target.back().push_back(cycle_hedges[i].first); + intervals.insert( std::make_pair(cycle_hedges[i-1].second, cycle_hedges[i].second) ); + std::size_t previous = cycle_hedges[i].second; while(++i!=nbv) { - if ( get(vpm, target(cycle_hedges[i], pm)) == - get(vpm, target(cycle_hedges[i-1], pm)) ) - hedges_with_identical_point_target.back().push_back(cycle_hedges[i]); + if ( get(vpm, target(cycle_hedges[i].first, pm)) == + get(vpm, target(cycle_hedges[i-1].first, pm)) ) + { + hedges_with_identical_point_target.back().push_back(cycle_hedges[i].first); + intervals.insert( std::make_pair(previous, cycle_hedges[i].second) ); + previous = cycle_hedges[i].second; + } else { ++i; @@ -141,6 +154,27 @@ void detect_identical_vertices(std::vector& cycle_hedges, else ++i; } + + // check that intervals are disjoint or strictly nested + // if there is only one issue we drop the whole cycle. + /// \todo shall we try to be more conservative? + if (hedges_with_identical_point_target.empty()) return; + std::set< std::pair >::iterator it1 = intervals.begin(), + end2 = intervals.end(), + end1 = cpp11::prev(end2), + it2; + for (; it1!=end1; ++it1) + for(it2=cpp11::next(it1); it2!= end2; ++it2 ) + { + CGAL_assertion(it1->firstfirst); + CGAL_assertion(it1->first < it1->second && it2->first < it2->second); + if (it1->second > it2->first && it2->second > it1->second) + { + std::cerr << "Merging is skipt to avoid bad cycle connections\n"; + hedges_with_identical_point_target.clear(); + return; + } + } } } // end of internal @@ -229,41 +263,24 @@ void merge_duplicated_vertices_in_boundary_cycle( get_const_property_map(vertex_point, pm)); // collect all the halfedges of the cycle - std::vector cycle_hedges; + std::vector< std::pair > cycle_hedges; halfedge_descriptor start=h; + std::size_t index=0; do{ - cycle_hedges.push_back(h); + cycle_hedges.push_back( std::make_pair(h, index) ); h=next(h, pm); + ++index; }while(start!=h); std::vector< std::vector > hedges_with_identical_point_target; - internal::detect_identical_vertices(cycle_hedges, hedges_with_identical_point_target, pm, vpm); + internal::detect_identical_mergeable_vertices(cycle_hedges, hedges_with_identical_point_target, pm, vpm); BOOST_FOREACH(const std::vector& hedges, hedges_with_identical_point_target) { start=hedges.front(); - // collect all halfedges in the cycle - std::vector sorted_hedges; - h=start; - do{ - sorted_hedges.push_back(h); - do - { - h=next(h, pm); - } - while( get(vpm, target(h, pm)) != get(vpm, target(start, pm)) ); - } - while(h!=start); - - if (sorted_hedges.size() != hedges.size()) - { - std::cerr << "WARNING: cycle broken at " << get(vpm, target(start, pm)) << ". Skipped\n"; - std::cout << sorted_hedges.size() << " vs " << hedges.size() << "\n"; - CGAL_assertion(sorted_hedges.size() == hedges.size()); - continue; - } - merge_boundary_vertices_in_cycle(sorted_hedges, pm); + // hedges are sorted along the cycle + merge_boundary_vertices_in_cycle(hedges, pm); } } From e299309a22a9598a6052fec3a75542113cdf2a63 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Tue, 22 May 2018 13:14:12 +0200 Subject: [PATCH 20/65] add missing named parameter documentation --- .../doc/Polygon_mesh_processing/NamedParameters.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt index 839bd7b52ff..002f2a837bb 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt @@ -365,6 +365,17 @@ should be considered as part of the clipping volume or not. \cgalNPEnd +\cgalNPBegin{output_iterator} \anchor PMP_output_iterator +Iterator where `std::vector` can be put. +The first vertex of the vector is an input vertex that was non-manifold, +the other vertices in the vertex are the new vertices created to fix +the non-manifoldness. +\n +\b Type : `iterator` \n +\b Default `Emptyset_iterator` +\cgalNPEnd + + \cgalNPTableEnd */ From b51fa000a47e41e078f259d07795393c33141e31 Mon Sep 17 00:00:00 2001 From: Konstantinos Katrioplas Date: Wed, 23 May 2018 12:17:46 +0200 Subject: [PATCH 21/65] documentation on merge border vertices functions --- .../merge_border_vertices.h | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 4a3ce2f8406..2406c6d72de 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -179,9 +179,18 @@ void detect_identical_mergeable_vertices( } // end of internal -/// \todo document me -/// It should probably go into BGL package -/// It should make sense to also return the length of each cycle +/// \ingroup PMP_repairing_grp +/// extracts boundary cycles as a list of halfedges. +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. +/// @tparam OutputIterator a model of `OutputIterator` holding objects of type +/// `boost::graph_traits::%halfedge_descriptor` +/// +/// @param pm the polygon mesh. +/// @param out an output iterator where the list of halfedges will be put. +/// +/// @todo Maybe move to BGL +/// @todo It should make sense to also return the length of each cycle. +/// @todo It should probably go into BGL package. template OutputIterator extract_boundary_cycles(PolygonMesh& pm, @@ -203,9 +212,16 @@ extract_boundary_cycles(PolygonMesh& pm, } /// \ingroup PMP_repairing_grp -/// \todo document me -/// we merge the all the target of the halfedges in `hedges` -/// hedges must be sorted along the cycle +/// merges target vertices of a list of halfedges. +/// Halfedges must be sorted in the list. +/// +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. +/// @tparam HalfedgeRange a range of halfedge descriptors of `PolygonMesh`, model of `Range`. +/// +/// @param sorted_hedges a sorted list of halfedges. +/// @param pm the polygon mesh which contains the list of halfedges. +/// +/// @todo rename me to `merge_vertices_in_range` because I merge any king of vertices in the list. template void merge_boundary_vertices_in_cycle(const HalfedgeRange& sorted_hedges, PolygonMesh& pm) @@ -249,7 +265,22 @@ void merge_boundary_vertices_in_cycle(const HalfedgeRange& sorted_hedges, } /// \ingroup PMP_repairing_grp -/// \todo document me +/// merges identical vertices around a cycle of connected edges. +/// +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. +/// @tparam NamedParameter a sequence of \ref pmp_namedparameters "Named Parameters". +/// +/// @param h a halfedge that belongs to the cycle. +/// @param pm the polygon mesh which containts the cycle. +/// @param np optional parameter of \ref pmp_namedparameters "Named Parameters" listed below. +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} +/// the property map with the points associated to the vertices of `pm`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalNamedParamsEnd template void merge_duplicated_vertices_in_boundary_cycle( typename boost::graph_traits::halfedge_descriptor h, @@ -285,7 +316,22 @@ void merge_duplicated_vertices_in_boundary_cycle( } /// \ingroup PMP_repairing_grp -/// \todo document me +/// extracts boundary cycles and merges the duplicated +/// vertices of each cycle. +/// +/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. +/// @tparam NamedParameter a sequence of \ref pmp_namedparameters "Named Parameters". +/// +/// @param pm the polygon mesh which containts the cycle. +/// @param np optional parameter of \ref pmp_namedparameters "Named Parameters" listed below. +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} +/// the property map with the points associated to the vertices of `pm`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamEnd +/// \cgalNamedParamsEnd template void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, const NamedParameter& np) From aed0cb1834060135476ffc921525e237502e7a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 3 Jul 2018 15:47:35 +0200 Subject: [PATCH 22/65] remove extra comma --- .../internal/Isotropic_remeshing/remesh_impl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h index 9686f4c271b..8826e7be23c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h @@ -365,7 +365,7 @@ namespace internal { BOOST_FOREACH(face_descriptor f, face_range) { - if (is_degenerate_triangle_face(f, mesh_,)){ + if (is_degenerate_triangle_face(f, mesh_)){ continue; } Patch_id pid = get_patch_id(f); From 0c9fea5d28f0dd5b543d1d073178fca81ec1a134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Thu, 19 Jul 2018 17:03:30 +0200 Subject: [PATCH 23/65] Fixed new named parameter test --- BGL/test/BGL/test_cgal_bgl_named_params.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BGL/test/BGL/test_cgal_bgl_named_params.cpp b/BGL/test/BGL/test_cgal_bgl_named_params.cpp index c0081450d1e..6bd99f2bdc0 100644 --- a/BGL/test/BGL/test_cgal_bgl_named_params.cpp +++ b/BGL/test/BGL/test_cgal_bgl_named_params.cpp @@ -163,7 +163,7 @@ void test(const NamedParameters& np) check_same_type<41>(get_param(np, CGAL::internal_np::verbosity_level)); check_same_type<42>(get_param(np, CGAL::internal_np::projection_functor)); check_same_type<46>(get_param(np, CGAL::internal_np::apply_per_connected_component)); - check_same_type472>(get_param(np, CGAL::internal_np::output_iterator)); + check_same_type<47>(get_param(np, CGAL::internal_np::output_iterator)); } int main() From 3b9464f54974b53bb230ee9c63861dffd06214e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Thu, 19 Jul 2018 17:05:07 +0200 Subject: [PATCH 24/65] Replaced No_constraint_pmap with Constant_property_map --- .../CGAL/Polygon_mesh_processing/helpers.h | 16 ----------- .../Isotropic_remeshing/remesh_impl.h | 17 +----------- .../random_perturbation.h | 4 +-- .../CGAL/Polygon_mesh_processing/remesh.h | 27 +++++++++---------- .../CGAL/Polygon_mesh_processing/repair.h | 7 ++--- Property_map/include/CGAL/property_map.h | 3 ++- 6 files changed, 22 insertions(+), 52 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h index ea38a91b835..b41c37e1c80 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -25,28 +25,12 @@ #include #include - namespace CGAL { namespace Polygon_mesh_processing { namespace internal { -template -struct No_constraint_pmap -{ -public: - typedef Descriptor key_type; - typedef bool value_type; - typedef value_type& reference; - typedef boost::read_write_property_map_tag category; - - friend bool get(const No_constraint_pmap& , const key_type& ) { - return false; - } - friend void put(No_constraint_pmap& , const key_type& , const bool ) {} -}; - template struct Vertex_collector { diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h index 8826e7be23c..428d49a6309 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h @@ -91,21 +91,6 @@ namespace internal { }; // A property map - template - struct No_constraint_pmap - { - public: - typedef Descriptor key_type; - typedef bool value_type; - typedef value_type& reference; - typedef boost::read_write_property_map_tag category; - - friend bool get(const No_constraint_pmap& , const key_type& ) { - return false; - } - friend void put(No_constraint_pmap& , const key_type& , const bool ) {} - }; - template struct Border_constraint_pmap { @@ -1514,7 +1499,7 @@ private: // update status using constrained edge map if (!boost::is_same >::value) + Constant_property_map >::value) { BOOST_FOREACH(edge_descriptor e, edges(mesh_)) { diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/random_perturbation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/random_perturbation.h index ea7362e1a95..8a210843b1e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/random_perturbation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/random_perturbation.h @@ -174,10 +174,10 @@ void random_perturbation(VertexRange vertices typedef typename boost::lookup_named_param_def < internal_np::vertex_is_constrained_t, NamedParameters, - internal::No_constraint_pmap//default + Constant_property_map // default > ::type VCMap; VCMap vcmap = choose_param(get_param(np, internal_np::vertex_is_constrained), - internal::No_constraint_pmap()); + Constant_property_map(false)); unsigned int seed = choose_param(get_param(np, internal_np::random_seed), -1); bool do_project = choose_param(get_param(np, internal_np::do_project), true); diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h index d87cd651b6a..40994d53a87 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -175,18 +175,18 @@ void isotropic_remeshing(const FaceRange& faces typedef typename boost::lookup_named_param_def < internal_np::edge_is_constrained_t, NamedParameters, - internal::No_constraint_pmap//default + Constant_property_map // default (no constraint pmap) > ::type ECMap; - ECMap ecmap = choose_param(get_param(np, internal_np::edge_is_constrained) - , internal::No_constraint_pmap()); + ECMap ecmap = choose_param(get_param(np, internal_np::edge_is_constrained), + Constant_property_map(false)); typedef typename boost::lookup_named_param_def < internal_np::vertex_is_constrained_t, NamedParameters, - internal::No_constraint_pmap//default + Constant_property_map // default (no constraint pmap) > ::type VCMap; VCMap vcmap = choose_param(get_param(np, internal_np::vertex_is_constrained), - internal::No_constraint_pmap()); + Constant_property_map(false)); bool protect = choose_param(get_param(np, internal_np::protect_constraints), false); typedef typename boost::lookup_named_param_def < @@ -351,22 +351,21 @@ void split_long_edges(const EdgeRange& edges typedef typename boost::lookup_named_param_def < internal_np::edge_is_constrained_t, NamedParameters, - internal::No_constraint_pmap//default + Constant_property_map // default (no constraint pmap) > ::type ECMap; ECMap ecmap = choose_param(get_param(np, internal_np::edge_is_constrained), - internal::No_constraint_pmap()); + Constant_property_map(false)); typename internal::Incremental_remesher, + Constant_property_map, // no constraint pmap internal::Connected_components_pmap, FIMap > - remesher(pmesh, vpmap, false/*protect constraints*/ - , ecmap - , internal::No_constraint_pmap() - , internal::Connected_components_pmap(faces(pmesh), pmesh, ecmap, fimap, false) - , fimap - , false/*need aabb_tree*/); + remesher(pmesh, vpmap, false/*protect constraints*/, ecmap, + Constant_property_map(false), + internal::Connected_components_pmap(faces(pmesh), pmesh, ecmap, fimap, false), + fimap, + false/*need aabb_tree*/); remesher.split_long_edges(edges, max_length); } diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index b5579b5204b..e03c785e990 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -24,12 +24,13 @@ #include - #include #include + #include #include #include +#include #include #include @@ -1319,11 +1320,11 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, typedef typename boost::lookup_named_param_def < internal_np::vertex_is_constrained_t, NamedParameters, - internal::No_constraint_pmap//default + Constant_property_map // default (no constraint pmap) > ::type VerticesMap; VerticesMap cmap = choose_param(get_param(np, internal_np::vertex_is_constrained), - internal::No_constraint_pmap()); + Constant_property_map(false)); typedef typename boost::lookup_named_param_def < internal_np::output_iterator_t, diff --git a/Property_map/include/CGAL/property_map.h b/Property_map/include/CGAL/property_map.h index f589496dcba..737c82320c3 100644 --- a/Property_map/include/CGAL/property_map.h +++ b/Property_map/include/CGAL/property_map.h @@ -462,10 +462,11 @@ make_property_map(const std::vector& v) template struct Constant_property_map { - const ValueType default_value; + ValueType default_value; typedef KeyType key_type; typedef ValueType value_type; + typedef value_type& reference; typedef boost::read_write_property_map_tag category; Constant_property_map(const value_type& default_value = value_type()) : default_value (default_value) { } From fc41d58bfdc150d9cffeca0e0f804445d0eac0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Fri, 20 Jul 2018 12:16:13 +0200 Subject: [PATCH 25/65] Added some missing \sa for Vector_23 --- Kernel_23/doc/Kernel_23/Concepts/GeomObjects.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Kernel_23/doc/Kernel_23/Concepts/GeomObjects.h b/Kernel_23/doc/Kernel_23/Concepts/GeomObjects.h index b120d75b38c..034890bb56c 100644 --- a/Kernel_23/doc/Kernel_23/Concepts/GeomObjects.h +++ b/Kernel_23/doc/Kernel_23/Concepts/GeomObjects.h @@ -721,6 +721,8 @@ public: \cgalHasModel `CGAL::Vector_2` \sa `Kernel::ComputeDeterminant_2` + \sa `Kernel::ComputeScalarProduct_2` + \sa `Kernel::ComputeSquaredLength_2` \sa `Kernel::ComputeX_2` \sa `Kernel::ComputeY_2` \sa `Kernel::ComputeHx_2` @@ -753,7 +755,10 @@ A type representing vectors in three dimensions. \cgalHasModel `CGAL::Vector_3` -\sa `Kernel::ComputeDeterminant_3` +\sa `Kernel::CompareDihedralAngle_3` +\sa `Kernel::ComputeDeterminant_3` +\sa `Kernel::ComputeScalarProduct_3` +\sa `Kernel::ComputeSquaredLength_3` \sa `Kernel::ComputeX_3` \sa `Kernel::ComputeY_3` \sa `Kernel::ComputeZ_3` From 49a971e9c2ae57864bbafccfc0b65c3a535c8a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Fri, 20 Jul 2018 17:30:40 +0200 Subject: [PATCH 26/65] Various improvements/fixes to degenerate/needle/cap functions --- .../NamedParameters.txt | 13 +- .../CGAL/Polygon_mesh_processing/helpers.h | 352 +++++++++--------- .../Isotropic_remeshing/remesh_impl.h | 88 ++--- .../CGAL/Polygon_mesh_processing/remesh.h | 7 +- .../CGAL/Polygon_mesh_processing/repair.h | 122 ++++-- .../data_degeneracies/caps_and_needles.off | 17 + .../test_predicates.cpp | 244 ++++++++---- .../Plugins/PMP/Selection_plugin.cpp | 4 +- .../demo/Polyhedron/Scene_polyhedron_item.cpp | 2 +- .../Scene_polyhedron_selection_item.cpp | 3 +- .../Polyhedron/Scene_surface_mesh_item.cpp | 2 +- .../include/CGAL/statistics_helpers.h | 25 +- 12 files changed, 519 insertions(+), 360 deletions(-) create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/caps_and_needles.off diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt index 002f2a837bb..a2c5456db34 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/NamedParameters.txt @@ -337,7 +337,7 @@ of a mesh independently.\n Parameter used to pass a visitor class to a function. Its type and behavior depend on the visited function. \n \b Type : `A class` \n -\b Default Specific to the function visited +\b Default : Specific to the function visited \cgalNPEnd \cgalNPBegin{throw_on_self_intersection} \anchor PMP_throw_on_self_intersection @@ -364,18 +364,13 @@ should be considered as part of the clipping volume or not. \b Default value is `true` \cgalNPEnd - \cgalNPBegin{output_iterator} \anchor PMP_output_iterator -Iterator where `std::vector` can be put. -The first vertex of the vector is an input vertex that was non-manifold, -the other vertices in the vertex are the new vertices created to fix -the non-manifoldness. +Parameter to pass an output iterator. \n -\b Type : `iterator` \n -\b Default `Emptyset_iterator` +\b Type : a model of `OutputIterator` \n +\b Default : `Emptyset_iterator` \cgalNPEnd - \cgalNPTableEnd */ diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h index b41c37e1c80..8e9cfeef182 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h @@ -1,4 +1,4 @@ -// Copyright (c) 2015 GeometryFactory (France). +// Copyright (c) 2015, 2018 GeometryFactory (France). // All rights reserved. // // This file is part of CGAL (www.cgal.org). @@ -17,7 +17,8 @@ // SPDX-License-Identifier: GPL-3.0+ // // -// Author(s) : Konstantinos Katrioplas +// Author(s) : Konstantinos Katrioplas, +// Mael Rouxel-Labbé #ifndef CGAL_POLYGON_MESH_PROCESSING_HELPERS_H #define CGAL_POLYGON_MESH_PROCESSING_HELPERS_H @@ -25,86 +26,40 @@ #include #include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + namespace CGAL { namespace Polygon_mesh_processing { -namespace internal { - -template -struct Vertex_collector -{ - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - void collect_vertices(vertex_descriptor v1, vertex_descriptor v2) - { - std::vector& verts = collections[v1]; - if (verts.empty()) - verts.push_back(v1); - verts.push_back(v2); - } - - void dump(OutputIterator out) - { - typedef std::pair > Pair_type; - BOOST_FOREACH(const Pair_type& p, collections) - { - *out++=p.second; - } - } - - std::map > collections; -}; - -template -struct Vertex_collector -{ - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - void collect_vertices(vertex_descriptor, vertex_descriptor) - {} - - void dump(Emptyset_iterator) - {} -}; - -// used only for testing -template -void merge_identical_points(PolygonMesh& mesh, - typename boost::graph_traits::vertex_descriptor v_keep, - typename boost::graph_traits::vertex_descriptor v_rm) -{ - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - halfedge_descriptor h = halfedge(v_rm, mesh); - halfedge_descriptor start = h; - - do{ - set_target(h, v_keep, mesh); - h = opposite(next(h, mesh), mesh); - } while( h != start ); - - remove_vertex(v_rm, mesh); -} -} // end internal - /// \ingroup PMP_repairing_grp -/// checks whether a vertex is non-manifold. +/// checks whether a vertex of a triangle mesh is non-manifold. /// -/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph` +/// @tparam TriangleMesh a model of `HalfedgeListGraph` /// -/// @param v the vertex -/// @param tm triangle mesh containing v +/// @param v a vertex of `tm` +/// @param tm a triangle mesh containing `v` /// -/// \return true if the vertrex is non-manifold -template -bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, - const PolygonMesh& tm) +/// \return `true` if the vertrex is non-manifold, `false` otherwise. +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const TriangleMesh& tm) { CGAL_assertion(CGAL::is_triangle_mesh(tm)); - typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; boost::unordered_set halfedges_handled; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) halfedges_handled.insert(h); @@ -121,28 +76,28 @@ bool is_non_manifold_vertex(typename boost::graph_traits::vertex_de /// \ingroup PMP_repairing_grp /// checks whether an edge is degenerate. -/// An edge is considered degenerate if the points of its vertices are identical. +/// An edge is considered degenerate if the geometric positions of its two extremities are identical. /// /// @tparam PolygonMesh a model of `HalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param e the edge -/// @param pm polygon mesh containing e +/// @param e an edge of `pm` +/// @param pm polygon mesh containing `e` /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `PolygonMesh` /// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`, -/// and the nested functor : -/// - `Equal_3` to check whether 2 points are identical +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor `Equal_3` to check whether two points are identical. /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the edge is degenerate +/// \return `true` if the edge `e` is degenerate, `false` otherwise. template bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, const PolygonMesh& pm, @@ -154,12 +109,11 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, pm)); + typedef typename GetGeomTraits::type Traits; Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); - if ( traits.equal_3_object()(get(vpmap, target(e, pm)), get(vpmap, source(e, pm))) ) - return true; - return false; + return traits.equal_3_object()(get(vpmap, source(e, pm)), get(vpmap, target(e, pm))); } template @@ -171,34 +125,34 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// \ingroup PMP_repairing_grp /// checks whether a triangle face is degenerate. -/// A triangle face is degenerate if its points are collinear. +/// A triangle face is considered degenerate if the geometric positions of its vertices are collinear. /// /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f the triangle face -/// @param tm triangle mesh containing f +/// @param f a triangle face of `tm` +/// @param tm a triangle mesh containing `f` /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `tm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`, -/// and the nested functor : -/// - `Collinear_3` to check whether 3 points are collinear +/// The traits class must provide the nested functor `Collinear_3` +/// to check whether three points are collinear. /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the triangle face is degenerate +/// \return `true` if the face `f` is degenerate, `false` otherwise. template bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, const TriangleMesh& tm, const NamedParameters& np) { - CGAL_assertion(CGAL::is_triangle_mesh(tm)); + CGAL_precondition(CGAL::is_triangle_mesh(tm)); using boost::get_param; using boost::choose_param; @@ -206,15 +160,15 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, tm)); + typedef typename GetGeomTraits::type Traits; Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); - typename boost::graph_traits::halfedge_descriptor hd = halfedge(f,tm); - const typename Traits::Point_3& p1 = get(vpmap, target( hd, tm) ); - const typename Traits::Point_3& p2 = get(vpmap, target(next(hd, tm), tm) ); - const typename Traits::Point_3& p3 = get(vpmap, source( hd, tm) ); - return traits.collinear_3_object()(p1, p2, p3); + typename boost::graph_traits::halfedge_descriptor h = halfedge(f, tm); + return traits.collinear_3_object()(get(vpmap, source(h, tm)), + get(vpmap, target(h, tm)), + get(vpmap, target(next(h, tm), tm))); } template @@ -226,159 +180,185 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac /// \ingroup PMP_repairing_grp /// checks whether a triangle face is needle. -/// A triangle is needle if its longest edge is much longer than the shortest one. +/// A triangle is said to be a needle if its longest edge is much longer than its shortest edge. /// /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f the triangle face -/// @param tm triangle mesh containing f -/// @param threshold the cosine of an angle of f. -/// The threshold is in range [0 1] and corresponds to -/// angles between 0 and 90 degrees. +/// @param f a triangle face of `tm` +/// @param tm triangle mesh containing `f` +/// @param threshold a bound on the ratio of the longest edge length and the shortest edge length /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `tm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`. +/// The traits class must provide the nested type `FT` and +/// the nested functor `Compute_squared_distance_3`. /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the triangle face is a needle +/// \return the smallest halfedge if the triangle face is a needle, and a null halfedge otherwise. template -bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold, - const NamedParameters& np) +typename boost::graph_traits::halfedge_descriptor +is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) { - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - CGAL_assertion(threshold >= 0); - CGAL_assertion(threshold <= 1); + CGAL_precondition(CGAL::is_triangle_mesh(tm)); + CGAL_precondition(threshold >= 1.); using boost::get_param; using boost::choose_param; + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - typedef typename Traits::FT FT; - typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; - typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::value_type Point_type; - typedef typename Kernel_traits::Kernel::Vector_3 Vector; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + typedef typename Traits::FT FT; + + const halfedge_descriptor h0 = halfedge(f, tm); + FT max_sq_length = - std::numeric_limits::max(), + min_sq_length = std::numeric_limits::max(); + halfedge_descriptor min_h = boost::graph_traits::null_halfedge(); + + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(h0, tm)) { - vertex_descriptor v0 = source(h, tm); - vertex_descriptor v1 = target(h, tm); - vertex_descriptor v2 = target(next(h, tm), tm); - Vector a = get(vpmap, v0) - get (vpmap, v1); - Vector b = get(vpmap, v2) - get(vpmap, v1); - FT aa = a.squared_length(); - FT bb = b.squared_length(); - FT squared_dot_ab = ((a*b)*(a*b)) / (aa * bb); + const FT sq_length = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), + get(vpmap, target(h, tm))); - if(squared_dot_ab > threshold * threshold) - return true; + if(max_sq_length < sq_length) + max_sq_length = sq_length; + + if(min_sq_length > sq_length) + { + min_h = h; + min_sq_length = sq_length; + } } - return false; + const FT sq_threshold = threshold * threshold; + if(max_sq_length / min_sq_length >= sq_threshold) + { + CGAL_assertion(min_h != boost::graph_traits::null_halfedge()); + return min_h; + } + else + return boost::graph_traits::null_halfedge(); } template -bool is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold) +typename boost::graph_traits::halfedge_descriptor +is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) { return is_needle_triangle_face(f, tm, threshold, parameters::all_default()); } /// \ingroup PMP_repairing_grp /// checks whether a triangle face is a cap. -/// A triangle is a cap if it has an angle very close to 180 degrees. +/// A triangle is said to be a cap if one of the its angles is close to `180` degrees. /// /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// -/// @param f the triangle face -/// @param tm triangle mesh containing f -/// @param threshold the cosine of an angle of f. -/// The threshold is in range [-1 0] and corresponds to -/// angles between 90 and 180 degrees. +/// @param f a triangle face of `tm` +/// @param tm triangle mesh containing `f` +/// @param threshold the cosine of a minimum angle such that if `f` has an angle greater than this bound, +/// it is a cap. The threshold is in range `[-1 0]` and corresponds to an angle +/// between `90` and `180` degrees. /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `tm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3` +/// The traits class must provide the nested type `Point_3` /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return true if the triangle face is a cap +/// \return `true` if the triangle face is a cap template -bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold, - const NamedParameters& np) +typename boost::graph_traits::halfedge_descriptor +is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold, + const NamedParameters& np) { - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - CGAL_assertion(threshold >= -1); - CGAL_assertion(threshold <= 0); + CGAL_precondition(CGAL::is_triangle_mesh(tm)); + CGAL_precondition(threshold >= -1.); + CGAL_precondition(threshold <= 0.); using boost::get_param; using boost::choose_param; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename GetVertexPointMap::const_type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_const_property_map(vertex_point, tm)); - typedef typename GetGeomTraits::type Traits; - typedef typename Traits::FT FT; - typedef boost::graph_traits GT; - typedef typename GT::halfedge_descriptor halfedge_descriptor; - typedef typename GT::vertex_descriptor vertex_descriptor; - typedef typename boost::property_traits::value_type Point_type; - typedef typename Kernel_traits::Kernel::Vector_3 Vector; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(halfedge(f, tm), tm)) + typedef typename GetGeomTraits::type Traits; + Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); + + typedef typename Traits::FT FT; + typedef typename Traits::Vector_3 Vector_3; + + const FT sq_threshold = threshold * threshold; + const halfedge_descriptor h0 = halfedge(f, tm); + + cpp11::array sq_lengths; + int pos = 0; + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(h0, tm)) { - vertex_descriptor v0 = source(h, tm); - vertex_descriptor v1 = target(h, tm); - vertex_descriptor v2 = target(next(h, tm), tm); - Vector a = get(vpmap, v0) - get (vpmap, v1); - Vector b = get(vpmap, v2) - get(vpmap, v1); - FT aa = a.squared_length(); - FT bb = b.squared_length(); - FT squared_dot_ab = ((a*b)*(a*b)) / (aa * bb); - - if(squared_dot_ab > threshold * threshold) - return true; + sq_lengths[pos++] = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), + get(vpmap, target(h, tm))); } - return false; + + pos = 0; + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(h0, tm)) + { + const vertex_descriptor v0 = source(h, tm); + const vertex_descriptor v1 = target(h, tm); + const vertex_descriptor v2 = target(next(h, tm), tm); + const Vector_3 a = traits.construct_vector_3_object()(get(vpmap, v1), get(vpmap, v2)); + const Vector_3 b = traits.construct_vector_3_object()(get(vpmap, v1), get(vpmap, v0)); + const FT dot_ab = traits.compute_scalar_product_3_object()(a, b); + const bool neg_sp = (dot_ab <= 0); + const FT sq_a = sq_lengths[(pos+1)%3]; + const FT sq_b = sq_lengths[pos]; + const FT sq_cos = dot_ab * dot_ab / (sq_a * sq_b); + + if(neg_sp && sq_cos >= sq_threshold) + return prev(h, tm); + } + return boost::graph_traits::null_halfedge(); } template -bool is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, - const TriangleMesh& tm, - const double threshold) +typename boost::graph_traits::halfedge_descriptor +is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, + const TriangleMesh& tm, + const double threshold) { return is_cap_triangle_face(f, tm, threshold, parameters::all_default()); } - - - } } // end namespaces CGAL and PMP - - #endif // CGAL_POLYGON_MESH_PROCESSING_HELPERS_H - diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h index 428d49a6309..e5e914402db 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h @@ -311,6 +311,7 @@ namespace internal { public: Incremental_remesher(PolygonMesh& pmesh , VertexPointMap& vpmap + , const GeomTraits& gt , const bool protect_constraints , EdgeIsConstrainedMap ecmap , VertexIsConstrainedMap vcmap @@ -319,6 +320,7 @@ namespace internal { , const bool build_tree = true)//built by the remesher : mesh_(pmesh) , vpmap_(vpmap) + , gt_(gt) , build_tree_(build_tree) , has_border_(false) , input_triangles_() @@ -350,9 +352,10 @@ namespace internal { BOOST_FOREACH(face_descriptor f, face_range) { - if (is_degenerate_triangle_face(f, mesh_)){ + if(is_degenerate_triangle_face(f, mesh_, parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) continue; - } + Patch_id pid = get_patch_id(f); input_triangles_.push_back(triangle(f)); input_patch_ids_.push_back(pid); @@ -803,7 +806,8 @@ namespace internal { debug_status_map(); debug_self_intersections(); CGAL_assertion(0 == PMP::remove_degenerate_faces(mesh_, - PMP::parameters::vertex_point_map(vpmap_).geom_traits(GeomTraits()))); + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))); #endif } @@ -908,7 +912,7 @@ namespace internal { debug_status_map(); CGAL_assertion(0 == PMP::remove_degenerate_faces(mesh_ , PMP::parameters::vertex_point_map(vpmap_) - .geom_traits(GeomTraits()))); + .geom_traits(gt_))); debug_self_intersections(); #endif @@ -950,9 +954,9 @@ namespace internal { else if (is_on_patch(v)) { - Vector_3 vn = PMP::compute_vertex_normal(v, mesh_ - , PMP::parameters::vertex_point_map(vpmap_) - .geom_traits(GeomTraits())); + Vector_3 vn = PMP::compute_vertex_normal(v, mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_)); put(propmap_normals, v, vn); Vector_3 move = CGAL::NULL_VECTOR; @@ -1444,20 +1448,8 @@ private: if (f == boost::graph_traits::null_face()) return CGAL::NULL_VECTOR; - halfedge_descriptor hd = halfedge(f, mesh_); - typename boost::property_traits::reference - p = get(vpmap_, target(hd, mesh_)); - hd = next(hd,mesh_); - typename boost::property_traits::reference - q = get(vpmap_, target(hd, mesh_)); - hd = next(hd,mesh_); - typename boost::property_traits::reference - r =get(vpmap_, target(hd, mesh_)); - - if (GeomTraits().collinear_3_object()(p,q,r)) - return CGAL::NULL_VECTOR; - else - return PMP::compute_face_normal(f, mesh_, parameters::vertex_point_map(vpmap_)); + return PMP::compute_face_normal(f, mesh_, parameters::vertex_point_map(vpmap_) + .geom_traits(gt_)); } template @@ -1573,27 +1565,31 @@ private: const bool collapse_constraints) { CGAL_assertion_code(std::size_t nb_done = 0); + boost::unordered_set degenerate_faces; BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(halfedge(v, mesh_), mesh_)) { - if (is_border(h, mesh_)) - continue; - if (is_degenerate_triangle_face(face(h), mesh_)) + if(!is_border(h, mesh_) && + is_degenerate_triangle_face(face(h, mesh_), mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) degenerate_faces.insert(h); } + while(!degenerate_faces.empty()) { halfedge_descriptor h = *(degenerate_faces.begin()); degenerate_faces.erase(degenerate_faces.begin()); - if (!is_degenerate_triangle_face(face(h), mesh_)) + if (!is_degenerate_triangle_face(face(h, mesh_), mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) //this can happen when flipping h has consequences further in the mesh continue; //check that opposite is not also degenerate - if (degenerate_faces.find(opposite(h, mesh_)) != degenerate_faces.end()) - degenerate_faces.erase(opposite(h, mesh_)); + degenerate_faces.erase(opposite(h, mesh_)); if(is_border(h, mesh_)) continue; @@ -1639,11 +1635,15 @@ private: short_edges.insert(typename Bimap::value_type(hf, sqlen)); } - if (!is_border(hf, mesh_) - && is_degenerate_triangle_face(face(h), mesh_)) + if(!is_border(hf, mesh_) && + is_degenerate_triangle_face(face(hf, mesh_), mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) degenerate_faces.insert(hf); - if (!is_border(hfo, mesh_) - && is_degenerate_triangle_face(face(h), mesh_)) + if(!is_border(hfo, mesh_) && + is_degenerate_triangle_face(face(hfo, mesh_), mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) degenerate_faces.insert(hfo); break; @@ -1660,9 +1660,10 @@ private: BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(he, mesh_)) { - if (is_border(h, mesh_)) - continue; - if (is_degenerate_triangle_face(face(h), mesh_)) + if(!is_border(h, mesh_) && + is_degenerate_triangle_face(face(h, mesh_), mesh_, + parameters::vertex_point_map(vpmap_) + .geom_traits(gt_))) return true; } return false; @@ -1803,10 +1804,10 @@ private: { std::cout << "Test self intersections..."; std::vector > facets; - PMP::self_intersections( - mesh_, - std::back_inserter(facets), - PMP::parameters::vertex_point_map(vpmap_)); + PMP::self_intersections(mesh_, + std::back_inserter(facets), + PMP::parameters::vertex_point_map(vpmap_) + .geom_traits(gt_)); //CGAL_assertion(facets.empty()); std::cout << "done ("<< facets.size() <<" facets)." << std::endl; } @@ -1815,11 +1816,11 @@ private: { std::cout << "Test self intersections..."; std::vector > facets; - PMP::self_intersections( - faces_around_target(halfedge(v, mesh_), mesh_), - mesh_, - std::back_inserter(facets), - PMP::parameters::vertex_point_map(vpmap_)); + PMP::self_intersections(faces_around_target(halfedge(v, mesh_), mesh_), + mesh_, + std::back_inserter(facets), + PMP::parameters::vertex_point_map(vpmap_) + .geom_traits(gt_)); //CGAL_assertion(facets.empty()); std::cout << "done ("<< facets.size() <<" facets)." << std::endl; } @@ -1902,6 +1903,7 @@ private: private: PolygonMesh& mesh_; VertexPointMap& vpmap_; + const GeomTraits& gt_; bool build_tree_; bool has_border_; std::vector trees; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h index 40994d53a87..b50060998f0 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -163,6 +163,7 @@ void isotropic_remeshing(const FaceRange& faces boost::is_default_param(get_param(np, internal_np::projection_functor)); typedef typename GetGeomTraits::type GT; + GT gt = choose_param(get_param(np, internal_np::geom_traits), GT()); typedef typename GetVertexPointMap::type VPMap; VPMap vpmap = choose_param(get_param(np, internal_np::vertex_point), @@ -227,7 +228,7 @@ void isotropic_remeshing(const FaceRange& faces #endif typename internal::Incremental_remesher - remesher(pmesh, vpmap, protect, ecmap, vcmap, fpmap, fimap, need_aabb_tree); + remesher(pmesh, vpmap, gt, protect, ecmap, vcmap, fpmap, fimap, need_aabb_tree); remesher.init_remeshing(faces); #ifdef CGAL_PMP_REMESHING_VERBOSE @@ -340,6 +341,8 @@ void split_long_edges(const EdgeRange& edges using boost::get_param; typedef typename GetGeomTraits::type GT; + GT gt = choose_param(get_param(np, internal_np::geom_traits), GT()); + typedef typename GetVertexPointMap::type VPMap; VPMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_property_map(vertex_point, pmesh)); @@ -361,7 +364,7 @@ void split_long_edges(const EdgeRange& edges internal::Connected_components_pmap, FIMap > - remesher(pmesh, vpmap, false/*protect constraints*/, ecmap, + remesher(pmesh, vpmap, gt, false/*protect constraints*/, ecmap, Constant_property_map(false), internal::Connected_components_pmap(faces(pmesh), pmesh, ecmap, fimap, false), fimap, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index e03c785e990..15c77eeb5c1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -159,17 +159,27 @@ struct Less_vertex_point{ } }; +template +OutputIterator +degenerate_faces(const TriangleMesh& tm, + OutputIterator out, + const NamedParameters& np) +{ + typedef typename boost::graph_traits::face_descriptor face_descriptor; + + BOOST_FOREACH(face_descriptor fd, faces(tm)) + { + if(is_degenerate_triangle_face(fd, tm, np)) + *out++ = fd; + } + return out; +} + template OutputIterator degenerate_faces(const TriangleMesh& tm, OutputIterator out) { - typedef typename boost::graph_traits::face_descriptor face_descriptor; - BOOST_FOREACH(face_descriptor fd, faces(tm)) - { - if ( is_degenerate_triangle_face(fd, tm) ) - *out++=fd; - } - return out; + return degenerate_faces(tm, out, CGAL::parameters::all_default()); } // this function remove a border edge even if it does not satisfy the link condition. @@ -717,9 +727,8 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, // Then, remove triangles made of 3 collinear points std::set degenerate_face_set; - BOOST_FOREACH(face_descriptor fd, faces(tmesh)) - if ( is_degenerate_triangle_face(fd, tmesh, np)) - degenerate_face_set.insert(fd); + degenerate_faces(tmesh, std::inserter(degenerate_face_set, degenerate_face_set.begin()), np); + nb_deg_faces+=degenerate_face_set.size(); // first remove degree 3 vertices that are part of a cap @@ -1274,6 +1283,45 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh) CGAL::Polygon_mesh_processing::parameters::all_default()); } +namespace internal { + +template +struct Vertex_collector +{ + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + + void collect_vertices(vertex_descriptor v1, vertex_descriptor v2) + { + std::vector& verts = collections[v1]; + if(verts.empty()) + verts.push_back(v1); + verts.push_back(v2); + } + + void dump(OutputIterator out) + { + typedef std::pair > Pair_type; + BOOST_FOREACH(const Pair_type& p, collections) { + *out++ = p.second; + } + } + + std::map > collections; +}; + +template +struct Vertex_collector +{ + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + void collect_vertices(vertex_descriptor, vertex_descriptor) + {} + + void dump(Emptyset_iterator) + {} +}; + +} // end namespace internal + /// \ingroup PMP_repairing_grp /// duplicates all non-manifold vertices of the input mesh. /// @@ -1284,22 +1332,22 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh) /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. +/// The type of this map is model of `ReadWritePropertyMap`. /// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` should be available in `PolygonMesh` +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{vertex_is_constrained_map} a writable property map with `vertex_descriptor` /// as key and `bool` as `value_type`. `put(pmap, v, true)` will be called for each duplicated /// vertices and the input one. /// \cgalParamEnd -/// \cgalParamBegin{output_iterator} an output iterator where `std::vector` can be put. -/// The first vertex of the vector is an input vertex that was non-manifold, -/// the other vertices in the vertex are the new vertices created to fix -/// the non-manifoldness. +/// \cgalParamBegin{output_iterator} a model of `OutputIterator` with value type +/// `std::vector`. The first vertex of the vector is a non-manifold vertex +/// of the input mesh, followed by the new vertices that were created to fix the non-manifoldness. /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return the number of vertices created +/// \return the number of vertices created. template std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, const NamedParameters& np) @@ -1315,7 +1363,7 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, typedef typename GetVertexPointMap::type VertexPointMap; VertexPointMap vpm = choose_param(get_param(np, internal_np::vertex_point), - get_property_map(vertex_point, tm)); + get_property_map(vertex_point, tm)); typedef typename boost::lookup_named_param_def < internal_np::vertex_is_constrained_t, @@ -1333,37 +1381,46 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, > ::type Output_iterator; Output_iterator out = choose_param(get_param(np, internal_np::output_iterator), - Emptyset_iterator()); + Emptyset_iterator()); internal::Vertex_collector dmap; boost::unordered_set vertices_handled; boost::unordered_set halfedges_handled; - std::size_t nb_new_vertices=0; + std::size_t nb_new_vertices = 0; std::vector non_manifold_cones; BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) { - if (halfedges_handled.insert(h).second) + // If 'h' is not visited yet, we walk around the target of 'h' and mark these + // halfedges as visited. Thus, if we are here and the target is already marked as visited, + // it means that the vertex is non manifold. + if(halfedges_handled.insert(h).second) { vertex_descriptor vd = target(h, tm); - if ( !vertices_handled.insert(vd).second ) + if(!vertices_handled.insert(vd).second) { put(cmap, vd, true); // store the originals non_manifold_cones.push_back(h); } else + { set_halfedge(vd, h, tm); - halfedge_descriptor start=opposite(next(h, tm), tm); - h=start; - do{ + } + + halfedge_descriptor start = opposite(next(h, tm), tm); + h = start; + do + { halfedges_handled.insert(h); - h=opposite(next(h, tm), tm); - }while(h!=start); + h = opposite(next(h, tm), tm); + } + while(h != start); } } - if (!non_manifold_cones.empty()) { + if(!non_manifold_cones.empty()) + { BOOST_FOREACH(halfedge_descriptor h, non_manifold_cones) { halfedge_descriptor start = h; @@ -1373,13 +1430,16 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm, dmap.collect_vertices(target(h, tm), new_vd); put(vpm, new_vd, get(vpm, target(h, tm))); set_halfedge(new_vd, h, tm); - do{ + do + { set_target(h, new_vd, tm); - h=opposite(next(h, tm), tm); - } while(h!=start); + h = opposite(next(h, tm), tm); + } + while(h != start); } dmap.dump(out); } + return nb_new_vertices; } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/caps_and_needles.off b/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/caps_and_needles.off new file mode 100644 index 00000000000..4e206746788 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/data_degeneracies/caps_and_needles.off @@ -0,0 +1,17 @@ +OFF +9 3 0 +0 0 0 +1 0 0 +1 1 0 +0 0 1 +1 0 1 +10 10 1 +0 0 2 +1 0 2 +-0.99619469809 0.08715574274 2 +3 0 1 2 +3 3 4 5 +3 6 7 8 + + + diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp index 3bf2ffcd494..4dd421fe533 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp @@ -1,142 +1,248 @@ #include + #include -#include -#include + #include #include -typedef CGAL::Exact_predicates_inexact_constructions_kernel K; -typedef CGAL::Surface_mesh Surface_mesh; +#include + +#include +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef K::FT FT; +typedef K::Point_3 Point_3; +typedef CGAL::Surface_mesh Surface_mesh; void check_edge_degeneracy(const char* fname) { - std::ifstream input(fname); + std::cout << "test edge degeneracy..."; + typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + + std::ifstream input(fname); Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; - exit(1); + std::exit(1); } - typedef typename boost::graph_traits::edge_descriptor edge_descriptor; std::vector all_edges(edges(mesh).begin(), edges(mesh).end()); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[0], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[1], mesh)); - CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[2], mesh)); + assert(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[0], mesh)); + assert(!CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[1], mesh)); + assert(CGAL::Polygon_mesh_processing::is_degenerate_edge(all_edges[2], mesh)); + std::cout << "done" << std::endl; } void check_triangle_face_degeneracy(const char* fname) { - std::ifstream input(fname); + std::cout << "test face degeneracy..."; + typedef typename boost::graph_traits::face_descriptor face_descriptor; + + std::ifstream input(fname); Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; - exit(1); + std::exit(1); } - typedef typename boost::graph_traits::face_descriptor face_descriptor; std::vector all_faces(faces(mesh).begin(), faces(mesh).end()); - CGAL_assertion(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[0], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[1], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[2], mesh)); - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); + assert(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[0], mesh)); + assert(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[1], mesh)); + assert(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[2], mesh)); + assert(!CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(all_faces[3], mesh)); + + std::cout << "done" << std::endl; } -// tests repair.h -void test_vertices_merge_and_duplication(const char* fname) +// tests merge_and_duplication +template +void merge_identical_points(typename boost::graph_traits::vertex_descriptor v_keep, + typename boost::graph_traits::vertex_descriptor v_rm, + PolygonMesh& mesh) { - std::ifstream input(fname); - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + + halfedge_descriptor h = halfedge(v_rm, mesh); + halfedge_descriptor start = h; + + do + { + set_target(h, v_keep, mesh); + h = opposite(next(h, mesh), mesh); } - const std::size_t initial_vertices = vertices(mesh).size(); + while( h != start ); - // create non-manifold vertex - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); - CGAL::Polygon_mesh_processing::internal::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); - - const std::size_t vertices_after_merge = vertices(mesh).size(); - CGAL_assertion(vertices_after_merge == initial_vertices - 1); - - std::vector< std::vector > duplicated_vertices; - CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh, - CGAL::parameters::output_iterator(std::back_inserter(duplicated_vertices))); - const std::size_t final_vertices_size = vertices(mesh).size(); - CGAL_assertion(final_vertices_size == vertices_after_merge + 1); - CGAL_assertion(final_vertices_size == initial_vertices); - CGAL_assertion(duplicated_vertices.size() == 2); + remove_vertex(v_rm, mesh); } void test_vertex_non_manifoldness(const char* fname) { + std::cout << "test vertex non manifoldness..."; + + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef typename boost::graph_traits::vertices_size_type size_type; + std::ifstream input(fname); Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; - exit(1); + std::exit(1); } + size_type ini_nv = num_vertices(mesh); + // create non-manifold vertex - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - std::vector all_vertices(vertices(mesh).begin(), vertices(mesh).end()); - CGAL::Polygon_mesh_processing::internal::merge_identical_points(mesh, all_vertices[1], all_vertices[7]); - std::vector vertices_with_non_manifold(vertices(mesh).begin(), vertices(mesh).end()); - CGAL_assertion(vertices_with_non_manifold.size() == all_vertices.size() - 1); + Surface_mesh::Vertex_index vertex_to_merge_onto(1); + Surface_mesh::Vertex_index vertex_to_merge(7); + merge_identical_points(vertex_to_merge_onto, vertex_to_merge, mesh); + mesh.collect_garbage(); - BOOST_FOREACH(std::size_t iv, vertices(mesh)) + assert(num_vertices(mesh) == ini_nv - 1); + + BOOST_FOREACH(vertex_descriptor v, vertices(mesh)) { - vertex_descriptor v = vertices_with_non_manifold[iv]; - if(iv == 1) - CGAL_assertion(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); + if(v == vertex_to_merge_onto) + assert(CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); else - CGAL_assertion(!CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); + assert(!CGAL::Polygon_mesh_processing::is_non_manifold_vertex(v, mesh)); } + + std::cout << "done" << std::endl; } -void test_needle(const char* fname) +void test_vertices_merge_and_duplication(const char* fname) { + std::cout << "test non manifold vertex duplication..."; + + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + std::ifstream input(fname); Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; - exit(1); + std::exit(1); } + const std::size_t initial_vertices = num_vertices(mesh); - const double threshold = 0.8; - BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) - { - CGAL_assertion(CGAL::Polygon_mesh_processing::is_needle_triangle_face(f, mesh, threshold)); - } + // create non-manifold vertex + Surface_mesh::Vertex_index vertex_to_merge_onto(1); + Surface_mesh::Vertex_index vertex_to_merge(7); + Surface_mesh::Vertex_index vertex_to_merge_2(14); + Surface_mesh::Vertex_index vertex_to_merge_3(21); + + Surface_mesh::Vertex_index vertex_to_merge_onto_2(2); + Surface_mesh::Vertex_index vertex_to_merge_4(8); + + merge_identical_points(vertex_to_merge_onto, vertex_to_merge, mesh); + merge_identical_points(vertex_to_merge_onto, vertex_to_merge_2, mesh); + merge_identical_points(vertex_to_merge_onto, vertex_to_merge_3, mesh); + merge_identical_points(vertex_to_merge_onto_2, vertex_to_merge_4, mesh); + mesh.collect_garbage(); + + const std::size_t vertices_after_merge = num_vertices(mesh); + assert(vertices_after_merge == initial_vertices - 4); + + std::vector > duplicated_vertices; + CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh, + CGAL::parameters::output_iterator(std::back_inserter(duplicated_vertices))); + + const std::size_t final_vertices_size = vertices(mesh).size(); + assert(final_vertices_size == initial_vertices); + assert(duplicated_vertices.size() == 2); // two non-manifold vertex + assert(duplicated_vertices.front().size() == 4); + assert(duplicated_vertices.back().size() == 2); + + std::cout << "done" << std::endl; } -void test_cap(const char* fname) +void test_needles_and_caps(const char* fname) { + std::cout << "test needles&caps..."; + + namespace PMP = CGAL::Polygon_mesh_processing; + + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::face_iterator face_iterator; + typedef typename boost::graph_traits::face_descriptor face_descriptor; + std::ifstream input(fname); Surface_mesh mesh; if (!input || !(input >> mesh) || mesh.is_empty()) { std::cerr << fname << " is not a valid off file.\n"; - exit(1); + std::exit(1); } - const double threshold = -0.8; - BOOST_FOREACH(typename boost::graph_traits::face_descriptor f, faces(mesh)) - { - CGAL_assertion(CGAL::Polygon_mesh_processing::is_cap_triangle_face(f, mesh, threshold)); - } + const FT eps = std::numeric_limits::epsilon(); + + face_iterator fit, fend; + boost::tie(fit, fend) = faces(mesh); + + // (0 0 0) -- (1 0 0) -- (1 1 0) (90° cap angle) + face_descriptor f = *fit; + halfedge_descriptor res = PMP::is_needle_triangle_face(f, mesh, 2/*needle_threshold*/); + assert(res == boost::graph_traits::null_halfedge()); // not a needle + res = PMP::is_needle_triangle_face(f, mesh, CGAL::sqrt(FT(2) - eps)/*needle_threshold*/); + assert(res != boost::graph_traits::null_halfedge()); // is a needle + + res = PMP::is_cap_triangle_face(f, mesh, 0./*cos(pi/2)*/); + assert(mesh.point(target(res, mesh)) == CGAL::ORIGIN); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(91 * CGAL_PI / 180)); + assert(res == boost::graph_traits::null_halfedge()); res = PMP::is_cap_triangle_face(f, mesh, std::cos(boost::math::constants::two_thirds_pi())); + assert(res == boost::graph_traits::null_halfedge()); + ++ fit; + + // (0 0 1) -- (1 0 1) -- (10 10 1) + f = *fit; + res = PMP::is_needle_triangle_face(f, mesh, 20); + assert(res == boost::graph_traits::null_halfedge()); + res = PMP::is_needle_triangle_face(f, mesh, 10 * CGAL::sqrt(FT(2) - eps)); + assert(mesh.point(target(res, mesh)) == Point_3(1,0,1)); + res = PMP::is_needle_triangle_face(f, mesh, 1); + assert(mesh.point(target(res, mesh)) == Point_3(1,0,1)); + + res = PMP::is_cap_triangle_face(f, mesh, 0./*cos(pi/2)*/); + assert(mesh.point(target(res, mesh)) == Point_3(0,0,1)); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(boost::math::constants::two_thirds_pi())); + assert(mesh.point(target(res, mesh)) == Point_3(0,0,1)); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(boost::math::constants::three_quarters_pi())); + assert(res == boost::graph_traits::null_halfedge()); + ++ fit; + + // (0 0 2) -- (1 0 2) -- (-0.99619469809 0.08715574274 2) (175° cap angle) + f = *fit; + res = PMP::is_needle_triangle_face(f, mesh, 2); + assert(res == boost::graph_traits::null_halfedge()); + res = PMP::is_needle_triangle_face(f, mesh, 1.9); + assert(mesh.point(target(res, mesh)) == Point_3(0,0,2) || + mesh.point(target(res, mesh)) == Point_3(1,0,2)); + res = PMP::is_needle_triangle_face(f, mesh, 1); + assert(mesh.point(target(res, mesh)) == Point_3(0,0,2) || + mesh.point(target(res, mesh)) == Point_3(1,0,2)); + + res = PMP::is_cap_triangle_face(f, mesh, 0./*cos(pi/2)*/); + assert(res != boost::graph_traits::null_halfedge() && + mesh.point(target(res, mesh)) != Point_3(0,0,2) && + mesh.point(target(res, mesh)) != Point_3(1,0,2)); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(boost::math::constants::two_thirds_pi())); + assert(res != boost::graph_traits::null_halfedge()); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(175 * CGAL_PI / 180)); + assert(res != boost::graph_traits::null_halfedge()); + res = PMP::is_cap_triangle_face(f, mesh, std::cos(176 * CGAL_PI / 180)); + assert(res == boost::graph_traits::null_halfedge()); + + std::cout << "done" << std::endl; } int main() { check_edge_degeneracy("data_degeneracies/degtri_edge.off"); check_triangle_face_degeneracy("data_degeneracies/degtri_four.off"); - test_vertices_merge_and_duplication("data_degeneracies/non_manifold_vertex_duplicated.off"); - test_vertex_non_manifoldness("data_degeneracies/non_manifold_vertex_duplicated.off"); - test_needle("data_degeneracies/needle.off"); - test_cap("data_degeneracies/cap.off"); + test_vertex_non_manifoldness("data/blobby.off"); + test_vertices_merge_and_duplication("data/blobby.off"); + test_needles_and_caps("data_degeneracies/caps_and_needles.off"); return 0; } diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp index ac1bfd837d4..28d0d5f779a 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp @@ -742,12 +742,10 @@ public Q_SLOTS: //Edition mode case 1: { - VPmap vpmap = get(CGAL::vertex_point, *selection_item->polyhedron()); bool is_valid = true; BOOST_FOREACH(boost::graph_traits::face_descriptor fd, faces(*selection_item->polyhedron())) { - if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(fd, - *selection_item->polyhedron())) + if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(fd, *selection_item->polyhedron())) { is_valid = false; break; diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp index 266f5331b94..a71037f40f7 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp @@ -1682,7 +1682,7 @@ QString Scene_polyhedron_item::computeStats(int type) if (d->poly->is_pure_triangle()) { if (d->number_of_degenerated_faces == (unsigned int)(-1)) - d->number_of_degenerated_faces = nb_degenerate_faces(d->poly, get(CGAL::vertex_point, *(d->poly))); + d->number_of_degenerated_faces = nb_degenerate_faces(d->poly); return QString::number(d->number_of_degenerated_faces); } else diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp index 488e874d416..6696a5ac9bc 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp @@ -2032,8 +2032,9 @@ bool Scene_polyhedron_selection_item_priv::canAddFace(fg_halfedge_descriptor hc, found = true; fg_halfedge_descriptor res = CGAL::Euler::add_face_to_border(t,hc, *item->polyhedron()); + fg_face_descriptor resf = face(res, *item->polyhedron()); - if(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(res, *item->polyhedron())) + if(CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(resf, *item->polyhedron())) { CGAL::Euler::remove_face(res, *item->polyhedron()); tempInstructions("Edge not selected : resulting facet is degenerated.", diff --git a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp index 75e60907f61..b2b56ff87d9 100644 --- a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp @@ -1502,7 +1502,7 @@ QString Scene_surface_mesh_item::computeStats(int type) if(is_triangle_mesh(*d->smesh_)) { if (d->number_of_degenerated_faces == (unsigned int)(-1)) - d->number_of_degenerated_faces = nb_degenerate_faces(d->smesh_, get(CGAL::vertex_point, *(d->smesh_))); + d->number_of_degenerated_faces = nb_degenerate_faces(d->smesh_); return QString::number(d->number_of_degenerated_faces); } else diff --git a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h index 711a641dcc3..e65bdf7329b 100644 --- a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h +++ b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h @@ -1,8 +1,6 @@ #ifndef POLYHEDRON_DEMO_STATISTICS_HELPERS_H #define POLYHEDRON_DEMO_STATISTICS_HELPERS_H -#include - #include #include #include @@ -10,12 +8,15 @@ #include #include #include -#include #include #include -#include +#include +#include +#include +#include +#include template void angles(Mesh* poly, double& mini, double& maxi, double& ave) @@ -84,19 +85,15 @@ void edges_length(Mesh* poly, mid = extract_result< tag::median >(acc); } -template -unsigned int nb_degenerate_faces(Mesh* poly, VPmap vpmap) +template +unsigned int nb_degenerate_faces(Mesh* poly) { typedef typename boost::graph_traits::face_descriptor face_descriptor; - typedef typename CGAL::Kernel_traits< typename boost::property_traits::value_type >::Kernel Traits; - unsigned int nb = 0; - BOOST_FOREACH(face_descriptor f, faces(*poly)) - { - if (CGAL::Polygon_mesh_processing::is_degenerate_triangle_face(f, *poly)) - ++nb; - } - return nb; + std::vector degenerate_faces; + CGAL::Polygon_mesh_processing::degenerate_faces(*poly, std::back_inserter(degenerate_faces)); + + return static_cast(degenerate_faces.size()); } template From 614f80694c6c95218560cce0717f2324fba669c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 11:36:15 +0200 Subject: [PATCH 27/65] Removed obsolete code about merging duplicated boundary vertices --- .../merge_border_vertices.h | 37 ------------------- .../test_merging_border_vertices.cpp | 36 +----------------- .../test_predicates.cpp | 2 +- 3 files changed, 3 insertions(+), 72 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 2406c6d72de..c530a82b51d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -345,35 +345,6 @@ void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, merge_duplicated_vertices_in_boundary_cycle(h, pm, np); } -#if 0 -/// \ingroup PMP_repairing_grp -/// \todo document me -template -void merge_duplicated_boundary_vertices( PolygonMesh& pm, - const NamedParameter& np) -{ - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - typedef typename GetVertexPointMap::const_type Vpm; - - Vpm vpm = choose_param(get_param(np, internal_np::vertex_point), - get_const_property_map(vertex_point, pm)); - - std::vector border_vertices; - BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) - { - if(is_border(h, pm)) - border_vertices.push_back(target(h, pm)); - } - - std::vector< std::vector > identical_vertices; - internal::detect_identical_vertices(border_vertices, identical_vertices, vpm); - - BOOST_FOREACH(const std::vector& vrtcs, identical_vertices) - merge_boundary_vertices(vrtcs, pm); -} -#endif - template void merge_duplicated_vertices_in_boundary_cycles(PolygonMesh& pm) { @@ -388,14 +359,6 @@ void merge_duplicated_vertices_in_boundary_cycle( merge_duplicated_vertices_in_boundary_cycle(h, pm, parameters::all_default()); } -#if 0 -template -void merge_duplicated_boundary_vertices(PolygonMesh& pm) -{ - merge_duplicated_boundary_vertices(pm, parameters::all_default()); -} -#endif - } } // end of CGAL::Polygon_mesh_processing #endif //CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp index a3052bbd2fd..ed21427f4d1 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_merging_border_vertices.cpp @@ -38,49 +38,17 @@ void test_merge_duplicated_vertices_in_boundary_cycles(const char* fname, } } -#if 0 -void test_merge_duplicated_boundary_vertices(const char* fname, - std::size_t expected_nb_vertices) -{ - std::ifstream input(fname); - - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << fname << " is not a valid off file.\n"; - exit(1); - } - - std::cout << "Testing merging globally " << fname << "\n"; - std::cout << " input mesh has " << vertices(mesh).size() << " vertices.\n"; - CGAL::Polygon_mesh_processing::merge_duplicated_boundary_vertices(mesh); - std::cout << " output mesh has " << vertices(mesh).size() << " vertices.\n"; - - assert(expected_nb_vertices == 0 || - expected_nb_vertices == vertices(mesh).size()); - if (expected_nb_vertices==0) - { - std::cout << "writting output to out2.off\n"; - std::ofstream output("out2.off"); - output << std::setprecision(17); - output << mesh; - } -} -#endif - int main(int argc, char** argv) { if (argc==1) { test_merge_duplicated_vertices_in_boundary_cycles("data/merge_points.off", 43); - // test_merge_duplicated_boundary_vertices("data/merge_points.off", 40); } else { for (int i=1; i< argc; ++i) - { test_merge_duplicated_vertices_in_boundary_cycles(argv[i], 0); - // test_merge_duplicated_boundary_vertices(argv[i], 0); - } } - return 0; + + return EXIT_SUCCESS; } diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp index 4dd421fe533..2124d1382ef 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp @@ -244,5 +244,5 @@ int main() test_vertices_merge_and_duplication("data/blobby.off"); test_needles_and_caps("data_degeneracies/caps_and_needles.off"); - return 0; + return EXIT_SUCCESS; } From a9897111c46092a5b820b6a90dc4fd3a3b935ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 12:14:11 +0200 Subject: [PATCH 28/65] Reorganized the new functions --- .../Isotropic_remeshing/remesh_impl.h | 2 +- .../CGAL/Polygon_mesh_processing/repair.h | 41 +++++++++++++++-- .../{helpers.h => shape_predicates.h} | 46 ++++--------------- .../Polygon_mesh_processing/CMakeLists.txt | 2 +- ...edicates.cpp => test_shape_predicates.cpp} | 2 +- .../Plugins/PMP/Degenerated_faces_plugin.cpp | 4 +- .../Plugins/PMP/Selection_plugin.cpp | 2 +- .../Edit_polyhedron_plugin.cpp | 2 +- .../demo/Polyhedron/Scene_polyhedron_item.cpp | 2 +- .../Scene_polyhedron_selection_item.cpp | 2 +- .../Polyhedron/Scene_surface_mesh_item.cpp | 2 +- 11 files changed, 57 insertions(+), 50 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{helpers.h => shape_predicates.h} (91%) rename Polygon_mesh_processing/test/Polygon_mesh_processing/{test_predicates.cpp => test_shape_predicates.cpp} (99%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h index e5e914402db..5ffc9952be8 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 15c77eeb5c1..26986cb8153 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -41,7 +41,7 @@ #include #include -#include +#include #include #include @@ -146,6 +146,8 @@ namespace debug{ } } //end of namespace debug +namespace internal { + template struct Less_vertex_point{ typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; @@ -159,6 +161,8 @@ struct Less_vertex_point{ } }; +} // end namespace internal + template OutputIterator degenerate_faces(const TriangleMesh& tm, @@ -959,7 +963,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, // preliminary step to check if the operation is possible // sort the boundary points along the common supporting line // we first need a reference point - typedef Less_vertex_point Less_vertex; + typedef internal::Less_vertex_point Less_vertex; std::pair< typename std::set::iterator, typename std::set::iterator > ref_vertices = @@ -1322,6 +1326,38 @@ struct Vertex_collector } // end namespace internal +/// \ingroup PMP_repairing_grp +/// checks whether a vertex of a triangle mesh is non-manifold. +/// +/// @tparam TriangleMesh a model of `HalfedgeListGraph` +/// +/// @param v a vertex of `tm` +/// @param tm a triangle mesh containing `v` +/// +/// \return `true` if the vertex is non-manifold, `false` otherwise. +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const TriangleMesh& tm) +{ + CGAL_assertion(CGAL::is_triangle_mesh(tm)); + + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + + boost::unordered_set halfedges_handled; + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) + halfedges_handled.insert(h); + + BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) + { + if(v == target(h, tm)) + { + if(halfedges_handled.count(h) == 0) + return true; + } + } + return false; +} + /// \ingroup PMP_repairing_grp /// duplicates all non-manifold vertices of the input mesh. /// @@ -1449,7 +1485,6 @@ std::size_t duplicate_non_manifold_vertices(TriangleMesh& tm) return duplicate_non_manifold_vertices(tm, parameters::all_default()); } - /// \ingroup PMP_repairing_grp /// removes the isolated vertices from any polygon mesh. /// A vertex is considered isolated if it is not incident to any simplex diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h similarity index 91% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 8e9cfeef182..dc66aecacf9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/helpers.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -20,8 +20,8 @@ // Author(s) : Konstantinos Katrioplas, // Mael Rouxel-Labbé -#ifndef CGAL_POLYGON_MESH_PROCESSING_HELPERS_H -#define CGAL_POLYGON_MESH_PROCESSING_HELPERS_H +#ifndef CGAL_POLYGON_MESH_PROCESSING_SHAPE_PREDICATES_H +#define CGAL_POLYGON_MESH_PROCESSING_SHAPE_PREDICATES_H #include #include @@ -42,38 +42,6 @@ namespace CGAL { namespace Polygon_mesh_processing { -/// \ingroup PMP_repairing_grp -/// checks whether a vertex of a triangle mesh is non-manifold. -/// -/// @tparam TriangleMesh a model of `HalfedgeListGraph` -/// -/// @param v a vertex of `tm` -/// @param tm a triangle mesh containing `v` -/// -/// \return `true` if the vertrex is non-manifold, `false` otherwise. -template -bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, - const TriangleMesh& tm) -{ - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - - boost::unordered_set halfedges_handled; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) - halfedges_handled.insert(h); - - BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) - { - if(v == target(h, tm)) - { - if(halfedges_handled.count(h) == 0) - return true; - } - } - return false; -} - /// \ingroup PMP_repairing_grp /// checks whether an edge is degenerate. /// An edge is considered degenerate if the geometric positions of its two extremities are identical. @@ -202,7 +170,7 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return the smallest halfedge if the triangle face is a needle, and a null halfedge otherwise. +/// \return the shortest halfedge if the triangle face is a needle, and a null halfedge otherwise. template typename boost::graph_traits::halfedge_descriptor is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, @@ -287,11 +255,13 @@ is_needle_triangle_face(typename boost::graph_traits::face_descrip /// `CGAL::vertex_point_t` should be available in `TriangleMesh` /// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3` +/// The traits class must provide the nested type `Point_3` and +/// the nested functors `Compute_squared_distance_3`, `Construct_vector_3`, +/// and `Compute_scalar_product_3`. /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \return `true` if the triangle face is a cap +/// \return the halfedge opposite of the largest angle if the face is a cap, and a null halfedge otherwise. template typename boost::graph_traits::halfedge_descriptor is_cap_triangle_face(typename boost::graph_traits::face_descriptor f, @@ -361,4 +331,4 @@ is_cap_triangle_face(typename boost::graph_traits::face_descriptor } } // end namespaces CGAL and PMP -#endif // CGAL_POLYGON_MESH_PROCESSING_HELPERS_H +#endif // CGAL_POLYGON_MESH_PROCESSING_SHAPE_PREDICATES_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt index 0c26363fca7..d8436cb8f30 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt @@ -102,7 +102,7 @@ endif() create_single_source_cgal_program("test_pmp_transform.cpp") create_single_source_cgal_program("remove_degeneracies_test.cpp") create_single_source_cgal_program("test_merging_border_vertices.cpp") - create_single_source_cgal_program("test_predicates.cpp") + create_single_source_cgal_program("test_shape_predicates.cpp") if( TBB_FOUND ) CGAL_target_use_TBB(test_pmp_distance) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp similarity index 99% rename from Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp rename to Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp index 2124d1382ef..7526ac80bec 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp @@ -2,8 +2,8 @@ #include -#include #include +#include #include diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp index b7c359e9ab1..9df12173d1f 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp @@ -19,7 +19,9 @@ #include #include #include -#include + +#include + #ifdef USE_SURFACE_MESH typedef Scene_surface_mesh_item Scene_facegraph_item; #else diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp index 28d0d5f779a..0b471fddc9b 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Selection_plugin.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include #ifdef USE_SURFACE_MESH diff --git a/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp index 18a5d1bb8e0..d0dab116457 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Surface_mesh_deformation/Edit_polyhedron_plugin.cpp @@ -9,7 +9,7 @@ #include "Scene_edit_polyhedron_item.h" #include "Scene_polyhedron_selection_item.h" #include -#include +#include #include #include #include diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp index a71037f40f7..df752a67272 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_item.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp index 6696a5ac9bc..d3d65dcd44c 100644 --- a/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_polyhedron_selection_item.cpp @@ -2,7 +2,7 @@ #include "Scene_polyhedron_selection_item.h" #include #include -#include +#include #include #include #include diff --git a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp index b2b56ff87d9..5b790daed6f 100644 --- a/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp +++ b/Polyhedron/demo/Polyhedron/Scene_surface_mesh_item.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include #include From 3934ad351e3de218049b7887ff6f0f9aa7bd6be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 16:03:05 +0200 Subject: [PATCH 29/65] Fixed BGL concepts --- BGL/doc/BGL/Concepts/EdgeListGraph.h | 8 ++++---- BGL/doc/BGL/PackageDescription.txt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BGL/doc/BGL/Concepts/EdgeListGraph.h b/BGL/doc/BGL/Concepts/EdgeListGraph.h index 7d7980aa172..d763d3a01fe 100644 --- a/BGL/doc/BGL/Concepts/EdgeListGraph.h +++ b/BGL/doc/BGL/Concepts/EdgeListGraph.h @@ -38,16 +38,16 @@ num_edges(const EdgeListGraph& g); /*! \relates EdgeListGraph -returns the source vertex of `h`. +returns the source vertex of `e`. */ template boost::graph_traits::vertex_descriptor -source(boost::graph_traits::halfedge_descriptor h, const EdgeListGraph& g); +source(boost::graph_traits::edge_descriptor e, const EdgeListGraph& g); /*! \relates EdgeListGraph -returns the target vertex of `h`. +returns the target vertex of `e`. */ template boost::graph_traits::vertex_descriptor -target(boost::graph_traits::halfedge_descriptor h, const EdgeListGraph& g); +target(boost::graph_traits::edge_descriptor e, const EdgeListGraph& g); diff --git a/BGL/doc/BGL/PackageDescription.txt b/BGL/doc/BGL/PackageDescription.txt index d40b1ec6ecf..86efff4c7a8 100644 --- a/BGL/doc/BGL/PackageDescription.txt +++ b/BGL/doc/BGL/PackageDescription.txt @@ -122,12 +122,12 @@ and adds the requirement for traversal of all edges in a graph. An upper bound of the number of edges of the graph - `source(g)` + `source(e, g)` `vertex_descriptor` The source vertex of `e` - `target(g)` + `target(e, g)` `vertex_descriptor` The target vertex of `e` From e3da86cff373a63ae8d840b22a79e346cc37c13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 16:07:33 +0200 Subject: [PATCH 30/65] Renamed removed_(null-->degenerate)_edges() for consistency --- .../CGAL/Polygon_mesh_processing/repair.h | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 26986cb8153..7323c9b73db 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -365,10 +365,9 @@ remove_a_border_edge(typename boost::graph_traits::edge_descriptor } template -std::size_t remove_null_edges( - const EdgeRange& edge_range, - TriangleMesh& tmesh, - const NamedParameters& np) +std::size_t remove_degenerate_edges(const EdgeRange& edge_range, + TriangleMesh& tmesh, + const NamedParameters& np) { CGAL_assertion(CGAL::is_triangle_mesh(tmesh)); @@ -385,27 +384,27 @@ std::size_t remove_null_edges( typedef typename GetVertexPointMap::type VertexPointMap; VertexPointMap vpmap = choose_param(get_param(np, internal_np::vertex_point), get_property_map(vertex_point, tmesh)); + typedef typename GetGeomTraits::type Traits; - Traits traits = choose_param(get_param(np, internal_np::geom_traits), Traits()); std::size_t nb_deg_faces = 0; // collect edges of length 0 - std::set null_edges_to_remove; + std::set degenerate_edges_to_remove; BOOST_FOREACH(edge_descriptor ed, edge_range) { - if ( traits.equal_3_object()(get(vpmap, target(ed, tmesh)), get(vpmap, source(ed, tmesh))) ) - null_edges_to_remove.insert(ed); + if(is_degenerate_edge(ed, tmesh, np)) + degenerate_edges_to_remove.insert(ed); } #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG - std::cout << "Found " << null_edges_to_remove.size() << " null edges.\n"; + std::cout << "Found " << degenerate_edges_to_remove.size() << " null edges.\n"; #endif - while (!null_edges_to_remove.empty()) + while (!degenerate_edges_to_remove.empty()) { - edge_descriptor ed = *null_edges_to_remove.begin(); - null_edges_to_remove.erase(null_edges_to_remove.begin()); + edge_descriptor ed = *degenerate_edges_to_remove.begin(); + degenerate_edges_to_remove.erase(degenerate_edges_to_remove.begin()); halfedge_descriptor h = halfedge(ed, tmesh); @@ -415,12 +414,12 @@ std::size_t remove_null_edges( if ( face(h, tmesh)!=GT::null_face() ) { ++nb_deg_faces; - null_edges_to_remove.erase(edge(prev(h, tmesh), tmesh)); + degenerate_edges_to_remove.erase(edge(prev(h, tmesh), tmesh)); } if (face(opposite(h, tmesh), tmesh)!=GT::null_face()) { ++nb_deg_faces; - null_edges_to_remove.erase(edge(prev(opposite(h, tmesh), tmesh), tmesh)); + degenerate_edges_to_remove.erase(edge(prev(opposite(h, tmesh), tmesh), tmesh)); } //now remove the edge CGAL::Euler::collapse_edge(ed, tmesh); @@ -435,7 +434,7 @@ std::size_t remove_null_edges( if (is_triangle(hd, tmesh)) { Euler::fill_hole(hd, tmesh); - null_edges_to_remove.insert(ed); + degenerate_edges_to_remove.insert(ed); continue; } } @@ -621,7 +620,7 @@ std::size_t remove_null_edges( // remove edges BOOST_FOREACH(edge_descriptor ed, edges_to_remove) { - null_edges_to_remove.erase(ed); + degenerate_edges_to_remove.erase(ed); remove_edge(ed, tmesh); } @@ -641,8 +640,8 @@ std::size_t remove_null_edges( put(vpmap, target(new_hd, tmesh), pt); BOOST_FOREACH(halfedge_descriptor hd, halfedges_around_target(new_hd, tmesh)) - if ( traits.equal_3_object()(get(vpmap, target(hd, tmesh)), get(vpmap, source(hd, tmesh))) ) - null_edges_to_remove.insert(edge(hd, tmesh)); + if(is_degenerate_edge(edge(hd, tmesh), tmesh, np)) + degenerate_edges_to_remove.insert(edge(hd, tmesh)); CGAL_assertion( is_valid_polygon_mesh(tmesh) ); } @@ -652,12 +651,10 @@ std::size_t remove_null_edges( } template -std::size_t remove_null_edges( - const EdgeRange& edge_range, - TriangleMesh& tmesh) +std::size_t remove_degenerate_edges(const EdgeRange& edge_range, + TriangleMesh& tmesh) { - return remove_null_edges(edge_range, tmesh, - parameters::all_default()); + return remove_degenerate_edges(edge_range, tmesh, parameters::all_default()); } /// \ingroup PMP_repairing_grp @@ -717,8 +714,9 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, typedef typename boost::property_traits::value_type Point_3; typedef typename boost::property_traits::reference Point_ref; + // First remove edges of length 0 - std::size_t nb_deg_faces = remove_null_edges(edges(tmesh), tmesh, np); + std::size_t nb_deg_faces = remove_degenerate_edges(edges(tmesh), tmesh, np); #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG { From 31609f2002f379a49291e8d1e498eec8e7b0771d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 16:31:51 +0200 Subject: [PATCH 31/65] Renamed function and removed obsolete code --- .../merge_border_vertices.h | 52 ++----------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index c530a82b51d..764022e0a6e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -37,48 +37,6 @@ namespace Polygon_mesh_processing{ namespace internal { -#if 0 -// warning: vertices will be altered (sorted) -template -void detect_identical_vertices(std::vector& vertices, - std::vector< std::vector >& identical_vertices, - Vpm vpm) -{ - typedef typename boost::property_traits::value_type Point_3; - - // sort vertices using their point to ease the detection - // of vertices with identical points - CGAL::Property_map_to_unary_function Get_point(vpm); - std::sort( vertices.begin(), vertices.end(), - boost::bind(std::less(), boost::bind(Get_point,_1), - boost::bind(Get_point, _2)) ); - std::size_t nbv=vertices.size(); - std::size_t i=1; - - while(i!=nbv) - { - if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) - { - identical_vertices.push_back( std::vector() ); - identical_vertices.back().push_back(vertices[i-1]); - identical_vertices.back().push_back(vertices[i]); - while(++i!=nbv) - { - if (get(vpm, vertices[i]) == get(vpm, vertices[i-1])) - identical_vertices.back().push_back(vertices[i]); - else - { - ++i; - break; - } - } - } - else - ++i; - } -} -#endif - template struct Less_on_point_of_target { @@ -188,7 +146,6 @@ void detect_identical_mergeable_vertices( /// @param pm the polygon mesh. /// @param out an output iterator where the list of halfedges will be put. /// -/// @todo Maybe move to BGL /// @todo It should make sense to also return the length of each cycle. /// @todo It should probably go into BGL package. template @@ -221,17 +178,16 @@ extract_boundary_cycles(PolygonMesh& pm, /// @param sorted_hedges a sorted list of halfedges. /// @param pm the polygon mesh which contains the list of halfedges. /// -/// @todo rename me to `merge_vertices_in_range` because I merge any king of vertices in the list. template -void merge_boundary_vertices_in_cycle(const HalfedgeRange& sorted_hedges, - PolygonMesh& pm) +void merge_vertices_in_range(const HalfedgeRange& sorted_hedges, + PolygonMesh& pm) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; halfedge_descriptor in_h_kept = *boost::begin(sorted_hedges); halfedge_descriptor out_h_kept = next(in_h_kept, pm); - vertex_descriptor v_kept=target(in_h_kept, pm); + vertex_descriptor v_kept = target(in_h_kept, pm); std::vector vertices_to_rm; @@ -311,7 +267,7 @@ void merge_duplicated_vertices_in_boundary_cycle( { start=hedges.front(); // hedges are sorted along the cycle - merge_boundary_vertices_in_cycle(hedges, pm); + merge_vertices_in_range(hedges, pm); } } From 018195d15df358c59a4ac9a613ce00c83c0fab20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 17:09:56 +0200 Subject: [PATCH 32/65] Documented 'degenerate_faces' and add 'degenerate_edges' --- .../CGAL/Polygon_mesh_processing/repair.h | 159 +++++++++++++++--- 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 7323c9b73db..f41ce6aa276 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -24,10 +24,6 @@ #include -#include -#include - -#include #include #include #include @@ -46,17 +42,28 @@ #include #include +#include + #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG #include #include -#include -#include #endif +#include +#include +#include + +#include +#include +#include +#include +#include +#include + namespace CGAL{ namespace Polygon_mesh_processing { - namespace debug{ + template std::ostream& dump_edge_neighborhood( typename boost::graph_traits::edge_descriptor ed, @@ -144,6 +151,7 @@ namespace debug{ << vids[ target(next(next(halfedge(f, tm), tm), tm), tm) ] << "\n"; } } + } //end of namespace debug namespace internal { @@ -163,15 +171,104 @@ struct Less_vertex_point{ } // end namespace internal +/// \ingroup PMP_repairing_grp +/// collects the degenerate edges within a given range of edges. +/// +/// @tparam EdgeRange a model of `Range` with value type `boost::graph_traits::edge_descriptor` +/// @tparam TriangleMesh a model of `EdgeListGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param edges a subset of edges of `tm` +/// @param tm a triangle mesh +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `tm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested type `Point_3`, +/// and the nested functor `Equal_3` to check whether two points are identical. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +template +OutputIterator degenerate_edges(const EdgeRange& edges, + const TriangleMesh& tm, + OutputIterator out, + const NamedParameters& np) +{ + typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + + BOOST_FOREACH(edge_descriptor ed, edges) + { + if(is_degenerate_edge(ed, tm, np)) + *out++ = ed; + } + return out; +} + +template +OutputIterator degenerate_edges(const EdgeRange& edges, + const TriangleMesh& tm, + OutputIterator out, + typename boost::disable_if_c< + CGAL::is_iterator::value + >::type* = 0) +{ + return degenerate_edges(edges, tm, out, CGAL::parameters::all_default()); +} + template +OutputIterator degenerate_edges(const TriangleMesh& tm, + OutputIterator out, + const NamedParameters& np, + typename boost::enable_if_c< + CGAL::is_iterator::value + >::type* = 0) +{ + return degenerate_edges(edges(tm), tm, out, np); +} + +template OutputIterator -degenerate_faces(const TriangleMesh& tm, - OutputIterator out, - const NamedParameters& np) +degenerate_edges(const TriangleMesh& tm, OutputIterator out) +{ + return degenerate_edges(edges(tm), tm, out, CGAL::parameters::all_default()); +} + +/// \ingroup PMP_repairing_grp +/// collects the degenerate faces within a given range of faces. +/// +/// @tparam FaceRange a model of `Range` with value type `boost::graph_traits::face_descriptor` +/// @tparam TriangleMesh a model of `FaceGraph` +/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +/// +/// @param faces a subset of faces of `tm` +/// @param tm a triangle mesh +/// @param np optional \ref pmp_namedparameters "Named Parameters" described below +/// +/// \cgalNamedParamsBegin +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `tm`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` should be available in `TriangleMesh` +/// \cgalParamEnd +/// \cgalParamBegin{geom_traits} a geometric traits class instance. +/// The traits class must provide the nested functor `Collinear_3` +/// to check whether three points are collinear. +/// \cgalParamEnd +/// \cgalNamedParamsEnd +template +OutputIterator degenerate_faces(const FaceRange& faces, + const TriangleMesh& tm, + OutputIterator out, + const NamedParameters& np) { typedef typename boost::graph_traits::face_descriptor face_descriptor; - BOOST_FOREACH(face_descriptor fd, faces(tm)) + BOOST_FOREACH(face_descriptor fd, faces) { if(is_degenerate_triangle_face(fd, tm, np)) *out++ = fd; @@ -179,11 +276,32 @@ degenerate_faces(const TriangleMesh& tm, return out; } -template -OutputIterator -degenerate_faces(const TriangleMesh& tm, OutputIterator out) +template +OutputIterator degenerate_faces(const FaceRange& faces, + const TriangleMesh& tm, + OutputIterator out, + typename boost::disable_if_c< + CGAL::is_iterator::value + >::type* = 0) { - return degenerate_faces(tm, out, CGAL::parameters::all_default()); + return degenerate_faces(faces, tm, out, CGAL::parameters::all_default()); +} + +template +OutputIterator degenerate_faces(const TriangleMesh& tm, + OutputIterator out, + const NamedParameters& np, + typename boost::enable_if_c< + CGAL::is_iterator::value + >::type* = 0) +{ + return degenerate_faces(faces(tm), tm, out, np); +} + +template +OutputIterator degenerate_faces(const TriangleMesh& tm, OutputIterator out) +{ + return degenerate_faces(faces(tm), tm, out, CGAL::parameters::all_default()); } // this function remove a border edge even if it does not satisfy the link condition. @@ -391,15 +509,12 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, // collect edges of length 0 std::set degenerate_edges_to_remove; - BOOST_FOREACH(edge_descriptor ed, edge_range) - { - if(is_degenerate_edge(ed, tmesh, np)) - degenerate_edges_to_remove.insert(ed); - } + degenerate_edges(edge_range, tmesh, std::inserter(degenerate_edges_to_remove, + degenerate_edges_to_remove.end())); - #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG +#ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG std::cout << "Found " << degenerate_edges_to_remove.size() << " null edges.\n"; - #endif +#endif while (!degenerate_edges_to_remove.empty()) { From 3d0c0d48d4f74c6a5758e68c1c8e8e6b9c72bde9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 17:28:36 +0200 Subject: [PATCH 33/65] Minor doc changes --- .../include/CGAL/Polygon_mesh_processing/repair.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index f41ce6aa276..4e1fe05fa1e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -786,15 +786,15 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` must be available in `TriangleMesh` -/// \cgalParamEnd +/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. +/// The type of this map is model of `ReadWritePropertyMap`. +/// If this parameter is omitted, an internal property map for +/// `CGAL::vertex_point_t` must be available in `TriangleMesh` +/// \cgalParamEnd /// \cgalParamBegin{geom_traits} a geometric traits class instance. /// The traits class must provide the nested type `Point_3`, /// and the nested functors : /// - `Compare_distance_3` to compute the distance between 2 points -/// - `Collinear_are_ordered_along_line_3` to check whether 3 collinear points are ordered /// - `Collinear_3` to check whether 3 points are collinear /// - `Less_xyz_3` to compare lexicographically two points /// - `Equal_3` to check whether 2 points are identical @@ -804,6 +804,7 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, /// /// \todo the function might not be able to remove all degenerate faces. /// We should probably do something with the return type. +/// /// \return number of removed degenerate faces template std::size_t remove_degenerate_faces(TriangleMesh& tmesh, @@ -1472,7 +1473,7 @@ bool is_non_manifold_vertex(typename boost::graph_traits::vertex_d } /// \ingroup PMP_repairing_grp -/// duplicates all non-manifold vertices of the input mesh. +/// duplicates all the non-manifold vertices of the input mesh. /// /// @tparam TriangleMesh a model of `HalfedgeListGraph` and `MutableHalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" From 6c0d6a79eb9372f7b1378cfe302e96f8055bc0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 17:28:44 +0200 Subject: [PATCH 34/65] Test degenerate_edges/faces --- .../remove_degeneracies_test.cpp | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp index 8b6488320e1..b37cc61eb92 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remove_degeneracies_test.cpp @@ -11,11 +11,34 @@ //the last test (on trihole.off) does not terminate // -typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +namespace PMP = CGAL::Polygon_mesh_processing; -typedef CGAL::Surface_mesh Surface_mesh; +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; -void fix(const char* fname) +typedef CGAL::Surface_mesh Surface_mesh; + +typedef boost::graph_traits::edge_descriptor edge_descriptor; +typedef boost::graph_traits::face_descriptor face_descriptor; + +void detect_degeneracies(const Surface_mesh& mesh) +{ + std::vector dfaces; + + PMP::degenerate_faces(mesh, std::back_inserter(dfaces)); + PMP::degenerate_faces(faces(mesh), mesh, std::back_inserter(dfaces)); + PMP::degenerate_faces(mesh, std::back_inserter(dfaces), CGAL::parameters::all_default()); + PMP::degenerate_faces(faces(mesh), mesh, std::back_inserter(dfaces), CGAL::parameters::all_default()); + assert(!dfaces.empty()); + + std::set dedges; + PMP::degenerate_edges(mesh, std::inserter(dedges, dedges.end())); + PMP::degenerate_edges(edges(mesh), mesh, std::inserter(dedges, dedges.begin())); + PMP::degenerate_edges(mesh, std::inserter(dedges, dedges.end()), CGAL::parameters::all_default()); + PMP::degenerate_edges(edges(mesh), mesh, std::inserter(dedges, dedges.begin()), CGAL::parameters::all_default()); + assert(dedges.empty()); +} + +void fix_degeneracies(const char* fname) { std::ifstream input(fname); @@ -24,21 +47,22 @@ void fix(const char* fname) std::cerr << fname << " is not a valid off file.\n"; exit(1); } - CGAL::Polygon_mesh_processing::remove_degenerate_faces(mesh); + detect_degeneracies(mesh); + + CGAL::Polygon_mesh_processing::remove_degenerate_faces(mesh); assert( CGAL::is_valid_polygon_mesh(mesh) ); } - int main() { - fix("data_degeneracies/degtri_2dt_1edge_split_twice.off"); - fix("data_degeneracies/degtri_four-2.off"); - fix("data_degeneracies/degtri_four.off"); - fix("data_degeneracies/degtri_on_border.off"); - fix("data_degeneracies/degtri_three.off"); - fix("data_degeneracies/degtri_single.off"); - fix("data_degeneracies/trihole.off"); + fix_degeneracies("data_degeneracies/degtri_2dt_1edge_split_twice.off"); + fix_degeneracies("data_degeneracies/degtri_four-2.off"); + fix_degeneracies("data_degeneracies/degtri_four.off"); + fix_degeneracies("data_degeneracies/degtri_on_border.off"); + fix_degeneracies("data_degeneracies/degtri_three.off"); + fix_degeneracies("data_degeneracies/degtri_single.off"); + fix_degeneracies("data_degeneracies/trihole.off"); return 0; } From 9bf6c331b946a2ab877b2614408ff16101329888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 17:42:11 +0200 Subject: [PATCH 35/65] Moved extract_boundary_cycles to border.h --- .../CGAL/Polygon_mesh_processing/border.h | 40 +++++++++++++++++-- .../merge_border_vertices.h | 36 ++--------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/border.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/border.h index 6c30cb6d077..dbe1f4945a7 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/border.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/border.h @@ -24,16 +24,16 @@ #include +#include +#include +#include +#include #include #include #include #include -#include -#include -#include - #include namespace CGAL{ @@ -257,6 +257,38 @@ namespace Polygon_mesh_processing { return border_counter; } + + /// @ingroup PkgPolygonMeshProcessing + /// extracts boundary cycles as a list of halfedges, with one halfedge per border. + /// + /// @tparam PolygonMesh a model of `HalfedgeListGraph` + /// @tparam OutputIterator a model of `OutputIterator` holding objects of type + /// `boost::graph_traits::%halfedge_descriptor` + /// + /// @param pm a polygon mesh + /// @param out an output iterator where the border halfedges will be put + /// + /// @todo It could make sense to also return the length of each cycle. + /// @todo It should probably go into BGL package (like the rest of this file). + template + OutputIterator extract_boundary_cycles(PolygonMesh& pm, + OutputIterator out) + { + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + + boost::unordered_set hedge_handled; + BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) + { + if(is_border(h, pm) && hedge_handled.insert(h).second) + { + *out++ = h; + BOOST_FOREACH(halfedge_descriptor h2, halfedges_around_face(h, pm)) + hedge_handled.insert(h2); + } + } + return out; + } + } // end of namespace Polygon_mesh_processing } // end of namespace CGAL diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 764022e0a6e..e58eae9d3da 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -31,9 +32,9 @@ #include #include -namespace CGAL{ +namespace CGAL { -namespace Polygon_mesh_processing{ +namespace Polygon_mesh_processing { namespace internal { @@ -137,37 +138,6 @@ void detect_identical_mergeable_vertices( } // end of internal -/// \ingroup PMP_repairing_grp -/// extracts boundary cycles as a list of halfedges. -/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. -/// @tparam OutputIterator a model of `OutputIterator` holding objects of type -/// `boost::graph_traits::%halfedge_descriptor` -/// -/// @param pm the polygon mesh. -/// @param out an output iterator where the list of halfedges will be put. -/// -/// @todo It should make sense to also return the length of each cycle. -/// @todo It should probably go into BGL package. -template -OutputIterator -extract_boundary_cycles(PolygonMesh& pm, - OutputIterator out) -{ - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - - boost::unordered_set hedge_handled; - BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) - { - if(is_border(h, pm) && hedge_handled.insert(h).second) - { - *out++=h; - BOOST_FOREACH(halfedge_descriptor h2, halfedges_around_face(h, pm)) - hedge_handled.insert(h2); - } - } - return out; -} - /// \ingroup PMP_repairing_grp /// merges target vertices of a list of halfedges. /// Halfedges must be sorted in the list. From 5db403ae343aabf2e227d30d50f18ac7196faeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 23 Jul 2018 17:44:04 +0200 Subject: [PATCH 36/65] Fixed indentation --- .../CGAL/Polygon_mesh_processing/merge_border_vertices.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index e58eae9d3da..6f0185533ef 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -71,8 +71,8 @@ template void detect_identical_mergeable_vertices( std::vector< std::pair >& cycle_hedges, std::vector< std::vector >& hedges_with_identical_point_target, - const PolygonMesh& pm, - Vpm vpm) + const PolygonMesh& pm, + Vpm vpm) { // sort vertices using their point to ease the detection // of vertices with identical points @@ -211,7 +211,7 @@ template void merge_duplicated_vertices_in_boundary_cycle( typename boost::graph_traits::halfedge_descriptor h, PolygonMesh& pm, - const NamedParameter& np) + const NamedParameter& np) { typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename GetVertexPointMap::const_type Vpm; From d56c12c738b78f600bf9b7c595a9b037503f8b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 11:14:47 +0200 Subject: [PATCH 37/65] Handle degenerate edges in 'is_needle_triangle_face' --- .../include/CGAL/Polygon_mesh_processing/shape_predicates.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index dc66aecacf9..7136db82cbd 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -171,6 +171,7 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac /// \cgalNamedParamsEnd /// /// \return the shortest halfedge if the triangle face is a needle, and a null halfedge otherwise. +/// If the face contains degenerate edges, a halfedge corresponding to one of these edges is returned. template typename boost::graph_traits::halfedge_descriptor is_needle_triangle_face(typename boost::graph_traits::face_descriptor f, @@ -215,6 +216,9 @@ is_needle_triangle_face(typename boost::graph_traits::face_descrip } } + if(min_sq_length == 0) + return min_h; + const FT sq_threshold = threshold * threshold; if(max_sq_length / min_sq_length >= sq_threshold) { From 16e64caf65ededeae63383c1227411f86a34d6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 11:20:04 +0200 Subject: [PATCH 38/65] Handle degenerate edges in 'is_cap_triangle_face' --- .../CGAL/Polygon_mesh_processing/shape_predicates.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 7136db82cbd..9673922342d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -300,8 +300,14 @@ is_cap_triangle_face(typename boost::graph_traits::face_descriptor int pos = 0; BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(h0, tm)) { - sq_lengths[pos++] = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), - get(vpmap, target(h, tm))); + const FT sq_d = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), + get(vpmap, target(h, tm))); + + // If even one edge is degenerate, it cannot be a cap + if(sq_d == 0) + return boost::graph_traits::null_halfedge(); + + sq_lengths[pos++] = sq_d; } pos = 0; @@ -316,7 +322,7 @@ is_cap_triangle_face(typename boost::graph_traits::face_descriptor const bool neg_sp = (dot_ab <= 0); const FT sq_a = sq_lengths[(pos+1)%3]; const FT sq_b = sq_lengths[pos]; - const FT sq_cos = dot_ab * dot_ab / (sq_a * sq_b); + const FT sq_cos = dot_ab * dot_ab / (sq_a * sq_b); if(neg_sp && sq_cos >= sq_threshold) return prev(h, tm); From e24b6c4dbfc7bd5f6f4a26c59fc94f626975ee57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 14:20:01 +0200 Subject: [PATCH 39/65] Revert "remove examples using a non documented function" This reverts commit f2882073bb4feb7250e190807cda836a09a84071. + updates --- .../doc/Polygon_mesh_processing/examples.txt | 2 + Polygon_mesh_processing/dont_submit | 2 + .../Polygon_mesh_processing/CMakeLists.txt | 6 +-- .../remove_degeneracies_example.cpp | 34 +++++++++++++++++ .../remove_degeneracies_example_OM.cpp | 38 +++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 Polygon_mesh_processing/dont_submit create mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp create mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt index 2811a37ecef..643c76c820b 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt @@ -21,4 +21,6 @@ \example Polygon_mesh_processing/corefinement_mesh_union_and_intersection.cpp \example Polygon_mesh_processing/corefinement_consecutive_bool_op.cpp \example Polygon_mesh_processing/detect_features_example.cpp +\example Polygon_mesh_processing/remove_degeneracies_example.cpp +\example Polygon_mesh_processing/remove_degeneracies_example_OM.cpp */ diff --git a/Polygon_mesh_processing/dont_submit b/Polygon_mesh_processing/dont_submit new file mode 100644 index 00000000000..9ee49f9ce2d --- /dev/null +++ b/Polygon_mesh_processing/dont_submit @@ -0,0 +1,2 @@ +examples/Polygon_mesh_processing/remove_degeneracies_example.cpp +examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index ed6a9a6e8f9..fac29293adf 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -89,7 +89,7 @@ create_single_source_cgal_program( "face_filtered_graph_example.cpp") create_single_source_cgal_program( "polygon_soup_example.cpp") create_single_source_cgal_program( "triangulate_polyline_example.cpp") create_single_source_cgal_program( "mesh_slicer_example.cpp") -#create_single_source_cgal_program( "remove_degeneracies_example.cpp") +create_single_source_cgal_program( "remove_degeneracies_example.cpp") create_single_source_cgal_program( "isotropic_remeshing_example.cpp") create_single_source_cgal_program( "isotropic_remeshing_of_patch_example.cpp") create_single_source_cgal_program( "surface_mesh_intersection.cpp") @@ -118,8 +118,8 @@ target_link_libraries( point_inside_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) create_single_source_cgal_program( "stitch_borders_example_OM.cpp" ) target_link_libraries( stitch_borders_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) -#create_single_source_cgal_program( "remove_degeneracies_example_OM.cpp") -#target_link_libraries( remove_degeneracies_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) +create_single_source_cgal_program( "remove_degeneracies_example_OM.cpp") +target_link_libraries( remove_degeneracies_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) create_single_source_cgal_program( "triangulate_faces_example_OM.cpp") target_link_libraries( triangulate_faces_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp new file mode 100644 index 00000000000..c79f9a5337f --- /dev/null +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp @@ -0,0 +1,34 @@ +#include + +#include + +#include + +#include +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +typedef CGAL::Surface_mesh Surface_mesh; + +int main(int argc, char* argv[]) +{ + const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; + std::ifstream input(filename); + + Surface_mesh mesh; + if (!input || !(input >> mesh) || mesh.is_empty()) { + std::cerr << "Not a valid .off file." << std::endl; + return EXIT_FAILURE; + } + + std::size_t nb = CGAL::Polygon_mesh_processing::remove_degenerate_faces(mesh); + + std::cout << "There were " << nb << " degenerate faces in this mesh" << std::endl; + + mesh.collect_garbage(); + std::ofstream out("repaired.off"); + out << mesh; + + return EXIT_SUCCESS; +} diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp new file mode 100644 index 00000000000..5fc249aec73 --- /dev/null +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp @@ -0,0 +1,38 @@ +#include + +#include +#include + +#include +#include + +#include +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; + +typedef OpenMesh::PolyMesh_ArrayKernelT< > Mesh; + +namespace PMP = CGAL::Polygon_mesh_processing; + +int main(int argc, char* argv[]) +{ + const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; + + Mesh mesh; + if (!input || !(OpenMesh::IO::read_mesh(mesh, filename)) || mesh.is_empty()) { + std::cerr << "Not a valid .off file." << std::endl; + return EXIT_FAILURE; + } + + std::size_t nb = PMP::remove_degenerate_faces(mesh, + CGAL::parameters::vertex_point_map(get(CGAL::vertex_point, mesh)) + .geom_traits(K())); + + std::cout << "There were " << nb << " degenerate faces in this mesh" << std::endl; + + mesh.garbage_collection(); + OpenMesh::IO::write_mesh(mesh, "repaired.off"); + + return EXIT_SUCCESS; +} From 64245daa4f3a445c43fba57d6fabc891341e103e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 14:55:20 +0200 Subject: [PATCH 40/65] Renamed PMP example to clarify use of orient functions --- .../doc/Polygon_mesh_processing/examples.txt | 2 +- .../examples/Polygon_mesh_processing/CMakeLists.txt | 2 +- ...polygon_soup_example.cpp => orient_polygon_soup_example.cpp} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename Polygon_mesh_processing/examples/Polygon_mesh_processing/{polygon_soup_example.cpp => orient_polygon_soup_example.cpp} (100%) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt index 643c76c820b..cb4ddd2ec62 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt @@ -8,7 +8,7 @@ \example Polygon_mesh_processing/triangulate_faces_example.cpp \example Polygon_mesh_processing/connected_components_example.cpp \example Polygon_mesh_processing/face_filtered_graph_example.cpp -\example Polygon_mesh_processing/polygon_soup_example.cpp +\example Polygon_mesh_processing/orient_polygon_soup_example.cpp \example Polygon_mesh_processing/triangulate_polyline_example.cpp \example Polygon_mesh_processing/refine_fair_example.cpp \example Polygon_mesh_processing/mesh_slicer_example.cpp diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index fac29293adf..6f083879d36 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -86,7 +86,7 @@ create_single_source_cgal_program( "point_inside_example.cpp") create_single_source_cgal_program( "triangulate_faces_example.cpp") create_single_source_cgal_program( "connected_components_example.cpp") create_single_source_cgal_program( "face_filtered_graph_example.cpp") -create_single_source_cgal_program( "polygon_soup_example.cpp") +create_single_source_cgal_program( "orient_polygon_soup_example.cpp") create_single_source_cgal_program( "triangulate_polyline_example.cpp") create_single_source_cgal_program( "mesh_slicer_example.cpp") create_single_source_cgal_program( "remove_degeneracies_example.cpp") diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/polygon_soup_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp similarity index 100% rename from Polygon_mesh_processing/examples/Polygon_mesh_processing/polygon_soup_example.cpp rename to Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp From 3866e720399218c340cc770d2e1f91ecae4a66d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 15:15:01 +0200 Subject: [PATCH 41/65] Updated orient_polygon_soup example to also showcase orient_to_bound_a_volume --- .../orient_polygon_soup_example.cpp | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp index 0c3a31a5344..3c073c82c8b 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/orient_polygon_soup_example.cpp @@ -1,34 +1,33 @@ #include + #include -#include +#include + #include #include #include +#include + #include #include #include -typedef CGAL::Exact_predicates_inexact_constructions_kernel K; -typedef CGAL::Polyhedron_3 Polyhedron; +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Polyhedron_3 Polyhedron; int main(int argc, char* argv[]) { const char* filename = (argc > 1) ? argv[1] : "data/tet-shuffled.off"; std::ifstream input(filename); - if (!input) + std::vector points; + std::vector > polygons; + + if(!input || !CGAL::read_OFF(input, points, polygons) || points.empty()) { std::cerr << "Cannot open file " << std::endl; - return 1; - } - - std::vector points; - std::vector< std::vector > polygons; - if (!CGAL::read_OFF(input, points, polygons)) - { - std::cerr << "Error parsing the OFF file " << std::endl; - return 1; + return EXIT_FAILURE; } CGAL::Polygon_mesh_processing::orient_polygon_soup(points, polygons); @@ -36,8 +35,13 @@ int main(int argc, char* argv[]) Polyhedron mesh; CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh(points, polygons, mesh); - if (CGAL::is_closed(mesh) && (!CGAL::Polygon_mesh_processing::is_outward_oriented(mesh))) - CGAL::Polygon_mesh_processing::reverse_face_orientations(mesh); + // Number the faces because 'orient_to_bound_a_volume' needs a face <--> index map + int index = 0; + for(Polyhedron::Face_iterator fb=mesh.facets_begin(), fe=mesh.facets_end(); fb!=fe; ++fb) + fb->id() = index++; + + if(CGAL::is_closed(mesh)) + CGAL::Polygon_mesh_processing::orient_to_bound_a_volume(mesh); std::ofstream out("tet-oriented1.off"); out << mesh; @@ -48,5 +52,5 @@ int main(int argc, char* argv[]) out2 << mesh; out2.close(); - return 0; + return EXIT_SUCCESS; } From e6d1977f7329603038bdd170cd83a0abfbae089c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 24 Jul 2018 18:07:49 +0200 Subject: [PATCH 42/65] Updated documentation --- .../PackageDescription.txt | 17 +- .../Polygon_mesh_processing.txt | 225 ++++++++++-------- .../merge_border_vertices.h | 5 +- .../CGAL/Polygon_mesh_processing/repair.h | 42 +++- .../shape_predicates.h | 5 + 5 files changed, 183 insertions(+), 111 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index 3baa705dad3..181eb3ddcd7 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -113,19 +113,31 @@ and provides a list of the parameters that are used in this package. - `CGAL::Polygon_mesh_processing::self_intersections()` - \link PMP_predicates_grp `CGAL::Polygon_mesh_processing::do_intersect()` \endlink - `CGAL::Polygon_mesh_processing::intersecting_meshes()` +- `CGAL::Polygon_mesh_processing::is_degenerate_edge()` +- `CGAL::Polygon_mesh_processing::degenerate_edges()` +- `CGAL::Polygon_mesh_processing::is_degenerate_triangle_face()` +- `CGAL::Polygon_mesh_processing::degenerate_faces()` +- `CGAL::Polygon_mesh_processing::is_needle_triangle_face()` +- `CGAL::Polygon_mesh_processing::is_cap_triangle_face()` ## Orientation Functions ## -- `CGAL::Polygon_mesh_processing::is_outward_oriented()` -- `CGAL::Polygon_mesh_processing::reverse_face_orientations()` - `CGAL::Polygon_mesh_processing::orient_polygon_soup()` - `CGAL::Polygon_mesh_processing::orient()` - `CGAL::Polygon_mesh_processing::orient_to_bound_a_volume()` +- `CGAL::Polygon_mesh_processing::is_outward_oriented()` +- `CGAL::Polygon_mesh_processing::reverse_face_orientations()` ## Combinatorial Repairing Functions ## - \link PMP_repairing_grp `CGAL::Polygon_mesh_processing::stitch_borders()` \endlink - `CGAL::Polygon_mesh_processing::is_polygon_soup_a_polygon_mesh()` - `CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh()` - `CGAL::Polygon_mesh_processing::remove_isolated_vertices()` +- `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()` +- `CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices()` +- `CGAL::Polygon_mesh_processing::remove_degenerate_faces()` +- `CGAL::Polygon_mesh_processing::merge_vertices_in_range()` +- `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()` +- `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles()` ## Normal Computation Functions ## - `CGAL::Polygon_mesh_processing::compute_face_normal()` @@ -179,6 +191,7 @@ and provides a list of the parameters that are used in this package. - `CGAL::Polygon_mesh_processing::edge_bbox()` - `CGAL::Polygon_mesh_processing::face_bbox()` - `CGAL::Polygon_mesh_processing::border_halfedges()` +- `CGAL::Polygon_mesh_processing::extract_boundary_cycles()` - `CGAL::Polygon_mesh_processing::transform()` */ diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt index e6f3c49f3b9..a9ea6529f7d 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt @@ -43,14 +43,14 @@ meshes, refinement, optimization by fairing, and isotropic remeshing of triangul - \ref Coref_section : methods to corefine triangle meshes and to compute boolean operations out of corefined closed triangle meshes. - \ref PMPHoleFilling : available hole filling algorithms, which can possibly be combined with refinement and fairing. -- \ref PMPPredicates : predicates that can be evaluated on the processed polygon +- \ref PMPPredicates : predicates that can be evaluated on the processed polygon. mesh, which includes point location and self intersection tests. -- \ref PMPOrientation : checking or fixing the \ref PMPOrientation of a polygon soup. +- \ref PMPOrientation : checking or fixing the orientation of a polygon soup. - \ref PMPRepairing : reparation of polygon meshes and polygon soups. - \ref PMPNormalComp : normal computation at vertices and on faces of a polygon mesh. - \ref PMPSlicer : functor able to compute the intersections of a polygon mesh with arbitrary planes (slicer). - \ref PMPConnectedComponents : methods to deal with connected - components of a polygon mesh (extraction, marks, removal, ...) + components of a polygon mesh (extraction, marks, removal, ...). **************************************** \section PMPMeshing Meshing @@ -309,7 +309,7 @@ This package provides an algorithm for filling one closed hole that is either in or defined by a sequence of points that describe a polyline. The main steps of the algorithm are described in \cgalCite{liepa2003filling} and can be summarized as follows. -First, the largest patch triangulating the boundary of the hole is generated without introducing any new vertex. +First, the largest patch triangulating the boundary of the hole is generated without introducing any new vertex. The patch is selected so as to minimize a quality function evaluated for all possible triangular patches. The quality function first minimizes the worst dihedral angle between patch triangles, then the total surface area of the patch as a tiebreaker. @@ -335,10 +335,10 @@ From left to right: (a) the hole, \subsection HoleFillingAPI API This package provides four functions for hole filling: - - `triangulate_hole_polyline()` : given a sequence of points defining the hole, triangulates the hole. - - `triangulate_hole()` : given a border halfedge on the boundary of the hole on a mesh, triangulates the hole. - - `triangulate_and_refine_hole()` : in addition to `triangulate_hole()` the generated patch is refined. - - `triangulate_refine_and_fair_hole()` : in addition to `triangulate_and_refine_hole()` the generated patch is also faired. + - `triangulate_hole_polyline()` : given a sequence of points defining the hole, triangulates the hole. + - `triangulate_hole()` : given a border halfedge on the boundary of the hole on a mesh, triangulates the hole. + - `triangulate_and_refine_hole()` : in addition to `triangulate_hole()` the generated patch is refined. + - `triangulate_refine_and_fair_hole()` : in addition to `triangulate_and_refine_hole()` the generated patch is also faired. \subsection HFExamples Examples @@ -372,51 +372,65 @@ iteratively filled, refined and faired to get a faired mesh with no hole. The hole filling algorithm has a complexity which depends on the number of vertices. While \cgalCite{liepa2003filling} has a running time of \f$ O(n^3)\f$ , \cgalCite{zou2013algorithm} in most cases has -running time of \f$ O(n \log n)\f$. We were running -`triangulate_refine_and_fair_hole()` for the below meshes (and two -more meshes with smaller holes). The machine used is a PC running -Windows 10 with an Intel Core i7 CPU clocked at 2.70 GHz. -The program has been compiled with Visual C++ 2013 compiler with the O2 -option which maximizes speed. +running time of \f$ O(n \log n)\f$. We benchmarked the function +`triangulate_refine_and_fair_hole()` for the two meshes below (as well as two +more meshes with smaller holes). The machine used was a PC running +Windows 10 with an Intel Core i7 CPU clocked at 2.70 GHz. +The program was compiled with the Visual C++ 2013 compiler with the O2 +option, which maximizes speed. \cgalFigureBegin{Elephants, elephants-with-holes.png} The elephant on the left/right has a hole with 963/7657 vertices. \cgalFigureEnd -This takes time +The following running times were observed: +
| # vertices | without Delaunay (sec.) | with Delaunay (sec.)| -| ----: | ----: | ----: | +| ----: | ----: | ----: | 565 | 8.5 | 0.03 | 774 | 21 | 0.035 | 967 | 43 | 0.06 | 7657 | na | 0.4 | - +
*************************************** \section PMPPredicates Predicates This packages provides several predicates to be evaluated with respect to a triangle mesh. -\subsection PMPSelIntersections Self Intersections +\subsection PMPDoIntersect Intersections Detection +Intersection tests between triangle meshes and/or polylines can be done using +\link PMP_predicates_grp `CGAL::Polygon_mesh_processing::do_intersect()` \endlink. +Additionally, the function `CGAL::Polygon_mesh_processing::intersecting_meshes()` +records all pairs of intersecting meshes in a range. -Self intersections can be detected from a triangle mesh, by calling the predicate +\subsubsection PMPSelIntersections Self Intersections + +Self intersections within a triangle mesh can be detected by calling the function `CGAL::Polygon_mesh_processing::does_self_intersect()`. Additionally, the function `CGAL::Polygon_mesh_processing::self_intersections()` reports all pairs of intersecting triangles. -\cgalFigureBegin{SelfIntersections, selfintersections.jpg} -Detecting self-intersections on a triangle mesh. -The intersecting triangles are displayed in dark grey on the right image. -\cgalFigureEnd - \subsubsection SIExample Self Intersections Example + +The following example illustrates the detection of self intersection in the `pig.off` mesh. +The detected self-intersection is illustrated on Figure \cgalFigureRef{SelfIntersections}. + \cgalExample{Polygon_mesh_processing/self_intersections_example.cpp} +\cgalFigureAnchor{SelfIntersections} +
+ +
+\cgalFigureCaptionBegin{SelfIntersections} +Detecting self-intersections on a triangle mesh. +The intersecting triangles are displayed in dark grey and red on the right image. +\cgalFigureCaptionEnd \subsection PMPInsideTest Side of Triangle Mesh -The class `CGAL::Side_of_triangle_mesh` provides a functor that tests whether a query point is +The class `CGAL::Side_of_triangle_mesh` provides a functor that tests whether a query point is inside, outside, or on the boundary of the domain bounded by a given closed triangle mesh. A point is said to be on the bounded side of the domain bounded by the input triangle mesh @@ -435,39 +449,90 @@ input triangle mesh. \subsubsection InsideExample Inside Test Example \cgalExample{Polygon_mesh_processing/point_inside_example.cpp} -\subsection PMPDoIntersect Intersections Detection -Intersection tests between triangle meshes and/or polylines can be done using -\link PMP_predicates_grp `CGAL::Polygon_mesh_processing::do_intersect()` \endlink. -Additionally, the function `CGAL::Polygon_mesh_processing::intersecting_meshes()` -records all pairs of intersecting meshes in a range. +\subsection PMPShapePredicates Shape Predicates + +Badly shaped or, even worse, completely degenerate elements of a polygon mesh are problematic +in many algorithms which one might want to use on the mesh. +This package offers a toolkit of functions to detect such undesirable elements. +- `CGAL::Polygon_mesh_processing::is_degenerate_edge()`, to detect if an edge is degenerate + (that is, if its two vertices share the same geometric location). +- `CGAL::Polygon_mesh_processing::is_degenerate_triangle_face()`, to detect if a face is + degenerate (that is, if its three vertices are collinear). +- `CGAL::Polygon_mesh_processing::degenerate_edges()`, to collect degenerate edges within a range of edges. +- `CGAL::Polygon_mesh_processing::degenerate_faces()`, to collect degenerate faces within a range of faces. +- `CGAL::Polygon_mesh_processing::is_cap_triangle_face()` +- `CGAL::Polygon_mesh_processing::is_needle_triangle_face()` + +This package offers functions to remove such undesirable elements, see Section \ref PMPRepairing. **************************************** \section PMPOrientation Orientation +This package offers multiple functions to compute consistent face orientations for set of faces +(Section \ref PolygonSoups) and polygon meshes (Section \ref OrientingPolygonMeshes). +Section \ref PolygonSoupExample offers an example of combination of these functions. + +\subsection PolygonSoups Polygon Soups + +When the faces of a polygon mesh are given but the connectivity is unknown, +this set of faces is called a \e polygon \e soup. + +Before running any of the algorithms on a polygon soup, +one should ensure that the polygons are consistently oriented. +To do so, this package provides the function +`CGAL::Polygon_mesh_processing::orient_polygon_soup()`, +described in \cgalCite{gueziec2001cutting}. + +To deal with polygon soups that cannot be converted to a +combinatorially manifold surface, some points must be duplicated. +Because a polygon soup does not have any connectivity (each point +has as many occurences as the number of polygons it belongs to), +duplicating one point (or a pair of points) +amounts to duplicating the polygon to which it belongs. +The duplicated points are either an endpoint of an edge incident to more +than two polygons, an endpoint of an edge between +two polygons with incompatible orientations (during the re-orientation process), +or more generally a point \a p at which the intersection +of an infinitesimally small ball centered at \a p +with the polygons incident to it is not a topological disk. + +Once the polygon soup is consistently oriented, +with possibly duplicated (or more) points, +the connectivity can be recovered and made consistent +to build a valid polygon mesh. +The function `CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh()` +performs this mesh construction step. + +\subsection OrientingPolygonMeshes Polygon Meshes + This package provides functions dealing with the orientation of faces in a closed polygon mesh. -The function `CGAL::Polygon_mesh_processing::is_outward_oriented()` checks whether +- The function `CGAL::Polygon_mesh_processing::orient()` makes each connected component +of a closed polygon mesh outward- or inward-oriented. +- The function `CGAL::Polygon_mesh_processing::orient_to_bound_a_volume()` orients +the connected components of a closed polygon mesh so that it bounds a volume +(see \ref coref_def_subsec for the precise definition). +- The function `CGAL::Polygon_mesh_processing::is_outward_oriented()` checks whether an oriented polygon mesh is oriented such that the normals to all faces are oriented towards the outside of the domain bounded by the input polygon mesh. - -The function -`CGAL::Polygon_mesh_processing::reverse_face_orientations()` reverses the orientation +- The function `CGAL::Polygon_mesh_processing::reverse_face_orientations()` reverses the orientation of halfedges around faces. As a consequence, the normal computed for each face (see Section \ref PMPNormalComp) is also reversed. -The \ref PolygonSoupExample puts these functions at work on a polygon soup. +\subsection PolygonSoupExample Orientation Example -The function `CGAL::Polygon_mesh_processing::orient()` makes each connected component -of a closed polygon mesh outward or inward oriented. - -The function `CGAL::Polygon_mesh_processing::orient_to_bound_a_volume()` orients -the connected components of a closed polygon mesh so that it bounds a volume -(see \ref coref_def_subsec for the precise definition). +This example shows how to generate a mesh from a polygon soup. +The first step is to get a soup of consistently oriented faces, before +rebuilding the connectivity. +In this example, some orientation tests are performed on the output +polygon mesh to illustrate +Section \ref PMPOrientation. +\cgalExample{Polygon_mesh_processing/orient_polygon_soup_example.cpp} **************************************** -\section PMPRepairing Combinatorial Repairing +\section PMPRepairing Combinatorial Repairing ******************* \subsection Stitching @@ -490,16 +555,12 @@ with duplicated border edges. \cgalExample{Polygon_mesh_processing/stitch_borders_example.cpp} -******************* -\cond \subsection DegenerateFaces Removing Degenerate Faces Some degenerate faces may be part of a given triangle mesh. A face is considered \e degenerate if two of its vertices -share the same location, -or in general if its three vertices are collinear. -The function -`CGAL::Polygon_mesh_processing::remove_degenerate_faces()` +share the same location, or more generally if its three vertices are collinear. +The function `CGAL::Polygon_mesh_processing::remove_degenerate_faces()` removes those faces and fixes the connectivity of the newly cleaned up mesh. It is also possible to remove isolated vertices from any polygon mesh, using the function `CGAL::Polygon_mesh_processing::remove_isolated_vertices()`. @@ -511,54 +572,26 @@ are removed, the connectivity is fixed, and the number of removed faces is output. \cgalExample{Polygon_mesh_processing/remove_degeneracies_example.cpp} -\endcond -******************* -\subsection PolygonSoups Polygon Soups +\subsection PMPManifoldness Polygon Mesh Manifoldness -When the faces of a polygon mesh are given but the connectivity is unknown, -we must deal with of a \e polygon \e soup. - -Before running any of the algorithms on the so-called -polygon soup, one should ensure that the polygons are consistently oriented. -To do so, this package provides the function -`CGAL::Polygon_mesh_processing::orient_polygon_soup()`, -described in \cgalCite{gueziec2001cutting}. - -To deal with polygon soups that cannot be converted to a -combinatorial manifold surface, some points are duplicated. -Because a polygon soup does not have any connectivity (each point -has as many occurences as the number of polygons it belongs to), -duplicating one point (or a pair of points) -amounts to duplicate the polygon to which it belongs. - -The duplicated points are either an endpoint of an edge incident to more -than two polygons, an endpoint of an edge between -two polygons with incompatible orientations (during the re-orientation process), -or more generally a point \a p at which the intersection -of an infinitesimally small ball centered at \a p -with the polygons incident to it is not a topological disk. - -Once the polygon soup is consistently oriented, -with possibly duplicated (or more) points, -the connectivity can be recovered and made consistent -to build a valid polygon mesh. -The function `CGAL::Polygon_mesh_processing::polygon_soup_to_polygon_mesh()` -performs this mesh construction step. - - -\subsubsection PolygonSoupExample Polygon Soup Example - -This example shows how to generate a mesh from a polygon soup. -The first step is to get a soup of consistently oriented faces, before -rebuilding the connectivity. -In this example, some orientation tests are performed on the output -polygon mesh to illustrate -Section \ref PMPOrientation. - -\cgalExample{Polygon_mesh_processing/polygon_soup_example.cpp} +Non-manifold vertices can be detected using the function `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()`. +The function `CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices()` can be used +to attempt to create a combinatorially manifold surface mesh by splitting any non-manifold vertex +into as many vertices as there are manifold sheets at this geometric position. +Note however that the mesh will still not be manifold from a geometric +point of view, as the positions of the new vertices introduced at a non-manifold vertex are identical +to the input non-manifold vertex. +\subsection PMPDuplicateVertexBoundaryCycle Duplicated Vertices in Boundary Cycles +Similarly to the problematic configuration of the previous section, another issue that can appear +in a polygon mesh is the occurrence of a "pinched" hole, that is the configuration where, when +starting from a border halfedge and walking the halfedges of this border, a geometric position appears +more than once (although, with different vertices) before reaching the initial border halfedge again. The functions +`CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()` and +`CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()`, which merge +vertices at identical positions, can be used to repair this configuration. **************************************** \section PMPNormalComp Computing Normals @@ -570,7 +603,7 @@ These computations are performed with : - `CGAL::Polygon_mesh_processing::compute_face_normal()` - `CGAL::Polygon_mesh_processing::compute_vertex_normal()` -We further provide functions to compute all the normals to faces, +Furthermore, we provide functions to compute all the normals to faces, or to vertices, or to both : - `CGAL::Polygon_mesh_processing::compute_face_normals()` - `CGAL::Polygon_mesh_processing::compute_vertex_normals()` @@ -578,14 +611,12 @@ or to vertices, or to both : Property maps are used to record the computed normals. - \subsection NormalsExample Normals Computation Examples -Property maps are an API introduced in the boost library, that allows to +Property maps are an API introduced in the boost library that allows to associate values to keys. In the following examples we associate a normal vector to each vertex and to each face. - \subsubsection NormalsExampleSM Normals Computation for a Surface Mesh The following example illustrates how to @@ -729,7 +760,7 @@ that respectively detect the sharp edges, compute the patch indices, and give ea \subsection DetectFeaturesExample Feature Detection Example In the following example, we count how many edges of `pmesh` are incident to two faces -which normals form an angle smaller than 90 degrees, +whose normals form an angle smaller than 90 degrees, and the number of surface patches that are separated by these edges. \cgalExample{Polygon_mesh_processing/detect_features_example.cpp} diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 6f0185533ef..07fe1728d46 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -242,8 +242,7 @@ void merge_duplicated_vertices_in_boundary_cycle( } /// \ingroup PMP_repairing_grp -/// extracts boundary cycles and merges the duplicated -/// vertices of each cycle. +/// extracts boundary cycles and merges the duplicated vertices of each cycle. /// /// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. /// @tparam NamedParameter a sequence of \ref pmp_namedparameters "Named Parameters". @@ -258,6 +257,8 @@ void merge_duplicated_vertices_in_boundary_cycle( /// `CGAL::vertex_point_t` should be available in `PolygonMesh` /// \cgalParamEnd /// \cgalNamedParamsEnd +/// +/// \sa `merge_duplicated_vertices_in_boundary_cycle()` template void merge_duplicated_vertices_in_boundary_cycles( PolygonMesh& pm, const NamedParameter& np) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 9f9a1d7a4e2..88a12acec66 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -174,12 +174,13 @@ struct Less_vertex_point{ /// \ingroup PMP_repairing_grp /// collects the degenerate edges within a given range of edges. /// -/// @tparam EdgeRange a model of `Range` with value type `boost::graph_traits::edge_descriptor` +/// @tparam EdgeRange a model of `Range` with value type `boost::graph_traits::%edge_descriptor` /// @tparam TriangleMesh a model of `EdgeListGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param edges a subset of edges of `tm` /// @param tm a triangle mesh +/// @param out an output iterator in which the degenerate edges are written /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -220,13 +221,21 @@ OutputIterator degenerate_edges(const EdgeRange& edges, return degenerate_edges(edges, tm, out, CGAL::parameters::all_default()); } +/// \ingroup PMP_repairing_grp +/// calls the function `degenerate_edges()` with the range: `edges(tm)`. +/// +/// See above for the comprehensive description of the parameters. +/// template OutputIterator degenerate_edges(const TriangleMesh& tm, OutputIterator out, - const NamedParameters& np, - typename boost::enable_if_c< + const NamedParameters& np +#ifndef DOXYGEN_RUNNING + , typename boost::enable_if_c< CGAL::is_iterator::value - >::type* = 0) + >::type* = 0 +#endif + ) { return degenerate_edges(edges(tm), tm, out, np); } @@ -241,12 +250,13 @@ degenerate_edges(const TriangleMesh& tm, OutputIterator out) /// \ingroup PMP_repairing_grp /// collects the degenerate faces within a given range of faces. /// -/// @tparam FaceRange a model of `Range` with value type `boost::graph_traits::face_descriptor` +/// @tparam FaceRange a model of `Range` with value type `boost::graph_traits::%face_descriptor` /// @tparam TriangleMesh a model of `FaceGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param faces a subset of faces of `tm` /// @param tm a triangle mesh +/// @param out an output iterator in which the degenerate faces are put /// @param np optional \ref pmp_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin @@ -260,6 +270,8 @@ degenerate_edges(const TriangleMesh& tm, OutputIterator out) /// to check whether three points are collinear. /// \cgalParamEnd /// \cgalNamedParamsEnd +/// +/// \sa remove_degenerate_faces() template OutputIterator degenerate_faces(const FaceRange& faces, const TriangleMesh& tm, @@ -287,13 +299,21 @@ OutputIterator degenerate_faces(const FaceRange& faces, return degenerate_faces(faces, tm, out, CGAL::parameters::all_default()); } +/// \ingroup PMP_repairing_grp +/// calls the function `degenerate_faces()` with the range: `faces(tm)`. +/// +/// See above for the comprehensive description of the parameters. +/// template OutputIterator degenerate_faces(const TriangleMesh& tm, OutputIterator out, - const NamedParameters& np, - typename boost::enable_if_c< + const NamedParameters& np +#ifndef DOXYGEN_RUNNING + , typename boost::enable_if_c< CGAL::is_iterator::value - >::type* = 0) + >::type* = 0 +#endif + ) { return degenerate_faces(faces(tm), tm, out, np); } @@ -798,7 +818,7 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, /// - `Collinear_3` to check whether 3 points are collinear /// - `Less_xyz_3` to compare lexicographically two points /// - `Equal_3` to check whether 2 points are identical -/// - for each functor Foo, a function `Foo foo_object()` +/// For each functor `Foo`, a function `Foo foo_object()` /// \cgalParamEnd /// \cgalNamedParamsEnd /// @@ -1448,6 +1468,8 @@ struct Vertex_collector /// @param v a vertex of `tm` /// @param tm a triangle mesh containing `v` /// +/// \sa duplicate_non_manifold_vertices() +/// /// \return `true` if the vertex is non-manifold, `false` otherwise. template bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, @@ -1489,7 +1511,7 @@ bool is_non_manifold_vertex(typename boost::graph_traits::vertex_d /// \cgalParamEnd /// \cgalParamBegin{vertex_is_constrained_map} a writable property map with `vertex_descriptor` /// as key and `bool` as `value_type`. `put(pmap, v, true)` will be called for each duplicated -/// vertices and the input one. +/// vertices, as well as the original non-manifold vertex in the input mehs. /// \cgalParamEnd /// \cgalParamBegin{output_iterator} a model of `OutputIterator` with value type /// `std::vector`. The first vertex of each vector is a non-manifold vertex diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 9673922342d..4cc98184b7f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -65,6 +65,8 @@ namespace Polygon_mesh_processing { /// \cgalParamEnd /// \cgalNamedParamsEnd /// +/// \sa `degenerate_edges()` +/// /// \return `true` if the edge `e` is degenerate, `false` otherwise. template bool is_degenerate_edge(typename boost::graph_traits::edge_descriptor e, @@ -114,6 +116,9 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// \cgalParamEnd /// \cgalNamedParamsEnd /// +/// \sa `degenerate_faces()` +/// \sa `remove_degenerate_faces()` +/// /// \return `true` if the face `f` is degenerate, `false` otherwise. template bool is_degenerate_triangle_face(typename boost::graph_traits::face_descriptor f, From 15b791901b692c1c4a71458639f95f26cad73e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 08:36:23 +0200 Subject: [PATCH 43/65] Fixed compilation error --- .../Polygon_mesh_processing/remove_degeneracies_example_OM.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp index 5fc249aec73..408990850de 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp @@ -20,7 +20,7 @@ int main(int argc, char* argv[]) const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; Mesh mesh; - if (!input || !(OpenMesh::IO::read_mesh(mesh, filename)) || mesh.is_empty()) { + if (!OpenMesh::IO::read_mesh(mesh, filename) || mesh.is_empty()) { std::cerr << "Not a valid .off file." << std::endl; return EXIT_FAILURE; } From 81d76c2e6903bfdac1835432a6ea9c09780fca50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 09:37:27 +0200 Subject: [PATCH 44/65] Added example about non-manifold vertex repair --- .../Polygon_mesh_processing.txt | 11 ++- .../doc/Polygon_mesh_processing/examples.txt | 1 + .../Polygon_mesh_processing/CMakeLists.txt | 1 + .../manifoldness_repair_example.cpp | 82 +++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/manifoldness_repair_example.cpp diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt index a9ea6529f7d..b7ca98e8808 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt @@ -46,7 +46,7 @@ meshes, refinement, optimization by fairing, and isotropic remeshing of triangul - \ref PMPPredicates : predicates that can be evaluated on the processed polygon. mesh, which includes point location and self intersection tests. - \ref PMPOrientation : checking or fixing the orientation of a polygon soup. -- \ref PMPRepairing : reparation of polygon meshes and polygon soups. +- \ref PMPRepairing : repair of polygon meshes and polygon soups. - \ref PMPNormalComp : normal computation at vertices and on faces of a polygon mesh. - \ref PMPSlicer : functor able to compute the intersections of a polygon mesh with arbitrary planes (slicer). - \ref PMPConnectedComponents : methods to deal with connected @@ -583,9 +583,16 @@ Note however that the mesh will still not be manifold from a geometric point of view, as the positions of the new vertices introduced at a non-manifold vertex are identical to the input non-manifold vertex. +\subsubsection FixNMVerticeExample Manifoldness Repair Example + +In the following example, a non-manifold configuration is artifically created and +fixed with the help of the functions described above. + +\cgalExample{Polygon_mesh_processing/manifoldness_repair_example.cpp} + \subsection PMPDuplicateVertexBoundaryCycle Duplicated Vertices in Boundary Cycles -Similarly to the problematic configuration of the previous section, another issue that can appear +Similarly to the problematic configuration described in the previous section, another issue that can be present in a polygon mesh is the occurrence of a "pinched" hole, that is the configuration where, when starting from a border halfedge and walking the halfedges of this border, a geometric position appears more than once (although, with different vertices) before reaching the initial border halfedge again. The functions diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt index cb4ddd2ec62..03ee471a773 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt @@ -23,4 +23,5 @@ \example Polygon_mesh_processing/detect_features_example.cpp \example Polygon_mesh_processing/remove_degeneracies_example.cpp \example Polygon_mesh_processing/remove_degeneracies_example_OM.cpp +\example Polygon_mesh_processing/manifoldness_repair_example.cpp */ diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index 6f083879d36..15092e7a30f 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -104,6 +104,7 @@ create_single_source_cgal_program( "random_perturbation_SM_example.cpp" ) create_single_source_cgal_program( "corefinement_LCC.cpp") create_single_source_cgal_program( "hole_filling_example_LCC.cpp" ) create_single_source_cgal_program( "detect_features_example.cpp" ) +create_single_source_cgal_program( "manifoldness_repair_example.cpp" ) if(OpenMesh_FOUND) create_single_source_cgal_program( "compute_normals_example_OM.cpp" ) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/manifoldness_repair_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/manifoldness_repair_example.cpp new file mode 100644 index 00000000000..64c3a91cab6 --- /dev/null +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/manifoldness_repair_example.cpp @@ -0,0 +1,82 @@ +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace PMP = CGAL::Polygon_mesh_processing; +namespace NP = CGAL::parameters; + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Surface_mesh Mesh; + +typedef boost::graph_traits::vertex_descriptor vertex_descriptor; +typedef boost::graph_traits::halfedge_descriptor halfedge_descriptor; + +void merge_vertices(vertex_descriptor v_keep, vertex_descriptor v_rm, Mesh& mesh) +{ + std::cout << "merging vertices " << v_keep << " and " << v_rm << std::endl; + + BOOST_FOREACH(halfedge_descriptor h, CGAL::halfedges_around_target(v_rm, mesh)){ + set_target(h, v_keep, mesh); // to ensure that no halfedge points at the deleted vertex + } + + remove_vertex(v_rm, mesh); +} + +int main(int argc, char* argv[]) +{ + const char* filename = (argc > 1) ? argv[1] : "data/blobby.off"; + std::ifstream input(filename); + + Mesh mesh; + if(!input || !(input >> mesh) || num_vertices(mesh) == 0) + { + std::cerr << filename << " is not a valid off file.\n"; + return EXIT_FAILURE; + } + + // Artificially create non-manifoldness for the sake of the example by merging some vertices + vertex_descriptor v0 = *(vertices(mesh).begin()); + vertex_descriptor v1 = *(--(vertices(mesh).end())); + merge_vertices(v0, v1, mesh); + + // Count non manifold vertices + int counter = 0; + BOOST_FOREACH(vertex_descriptor v, vertices(mesh)) + { + if(PMP::is_non_manifold_vertex(v, mesh)) + { + std::cout << "vertex " << v << " is non-manifold" << std::endl; + ++counter; + } + } + + std::cout << counter << " non-manifold occurrence(s)" << std::endl; + + // Fix manifoldness by splitting non-manifold vertices + std::vector > duplicated_vertices; + std::size_t new_vertices_nb = PMP::duplicate_non_manifold_vertices(mesh, + NP::output_iterator( + std::back_inserter(duplicated_vertices))); + + std::cout << new_vertices_nb << " vertices have been added to fix mesh manifoldness" << std::endl; + + for(std::size_t i=0; i Date: Wed, 25 Jul 2018 09:40:03 +0200 Subject: [PATCH 45/65] Minor test improvement --- .../test/Polygon_mesh_processing/test_shape_predicates.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp index 7526ac80bec..e68e8a7d09f 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp @@ -146,11 +146,13 @@ void test_vertices_merge_and_duplication(const char* fname) assert(vertices_after_merge == initial_vertices - 4); std::vector > duplicated_vertices; - CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh, - CGAL::parameters::output_iterator(std::back_inserter(duplicated_vertices))); + std::size_t new_vertices_nb = + CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices(mesh, + CGAL::parameters::output_iterator(std::back_inserter(duplicated_vertices))); const std::size_t final_vertices_size = vertices(mesh).size(); assert(final_vertices_size == initial_vertices); + assert(new_vertices_nb == 4); assert(duplicated_vertices.size() == 2); // two non-manifold vertex assert(duplicated_vertices.front().size() == 4); assert(duplicated_vertices.back().size() == 2); From 99864af9f5223a394d57126d49d7a0304e6119c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 09:40:17 +0200 Subject: [PATCH 46/65] Moved Degenerate Faces Removal and NM Vertex Repair from demo/experimental --- .../demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp index f2f7ddf66b7..c8b2349fe1c 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp @@ -54,11 +54,11 @@ public: actionAutorefine->setObjectName("actionAutorefine"); actionAutorefineAndRMSelfIntersections->setObjectName("actionAutorefineAndRMSelfIntersections"); - actionRemoveDegenerateFaces->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); + actionRemoveDegenerateFaces->setProperty("subMenuName", "Polygon Mesh Processing/Repair"); actionStitchCloseBorderHalfedges->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionRemoveSelfIntersections->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionRemoveIsolatedVertices->setProperty("subMenuName", "Polygon Mesh Processing/Repair"); - actionDuplicateNMVertices->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); + actionDuplicateNMVertices->setProperty("subMenuName", "Polygon Mesh Processing/Repair"); actionAutorefine->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionAutorefineAndRMSelfIntersections->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); From 5b22f7213e141ee71c004fab58a3f747b47a601b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 09:46:55 +0200 Subject: [PATCH 47/65] Fixed compilation error --- .../Polygon_mesh_processing/remove_degeneracies_example_OM.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp index 408990850de..46763164848 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp @@ -20,7 +20,7 @@ int main(int argc, char* argv[]) const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; Mesh mesh; - if (!OpenMesh::IO::read_mesh(mesh, filename) || mesh.is_empty()) { + if (!OpenMesh::IO::read_mesh(mesh, filename) || num_vertices(mesh)) { std::cerr << "Not a valid .off file." << std::endl; return EXIT_FAILURE; } From 0417bb88d77c652fd301787eb0b110ea062dd6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 10:51:53 +0200 Subject: [PATCH 48/65] Hide "remove_degenerate_faces" --- .../PackageDescription.txt | 1 - .../Polygon_mesh_processing.txt | 6 +- .../doc/Polygon_mesh_processing/examples.txt | 2 - .../Polygon_mesh_processing/CMakeLists.txt | 6 +- .../remove_degeneracies_example.cpp | 34 --------- .../remove_degeneracies_example_OM.cpp | 38 ---------- .../CGAL/Polygon_mesh_processing/repair.h | 69 +++++++++---------- .../shape_predicates.h | 1 - .../Plugins/PMP/Repair_polyhedron_plugin.cpp | 2 +- 9 files changed, 42 insertions(+), 117 deletions(-) delete mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp delete mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index 181eb3ddcd7..d937e76949b 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -134,7 +134,6 @@ and provides a list of the parameters that are used in this package. - `CGAL::Polygon_mesh_processing::remove_isolated_vertices()` - `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()` - `CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices()` -- `CGAL::Polygon_mesh_processing::remove_degenerate_faces()` - `CGAL::Polygon_mesh_processing::merge_vertices_in_range()` - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()` - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles()` diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt index b7ca98e8808..514d3b55397 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Polygon_mesh_processing.txt @@ -463,8 +463,6 @@ This package offers a toolkit of functions to detect such undesirable elements. - `CGAL::Polygon_mesh_processing::is_cap_triangle_face()` - `CGAL::Polygon_mesh_processing::is_needle_triangle_face()` -This package offers functions to remove such undesirable elements, see Section \ref PMPRepairing. - **************************************** \section PMPOrientation Orientation @@ -555,6 +553,8 @@ with duplicated border edges. \cgalExample{Polygon_mesh_processing/stitch_borders_example.cpp} +\cond + \subsection DegenerateFaces Removing Degenerate Faces Some degenerate faces may be part of a given triangle mesh. @@ -573,6 +573,8 @@ is output. \cgalExample{Polygon_mesh_processing/remove_degeneracies_example.cpp} +\endcond + \subsection PMPManifoldness Polygon Mesh Manifoldness Non-manifold vertices can be detected using the function `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()`. diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt index 03ee471a773..cb09da7d83f 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/examples.txt @@ -21,7 +21,5 @@ \example Polygon_mesh_processing/corefinement_mesh_union_and_intersection.cpp \example Polygon_mesh_processing/corefinement_consecutive_bool_op.cpp \example Polygon_mesh_processing/detect_features_example.cpp -\example Polygon_mesh_processing/remove_degeneracies_example.cpp -\example Polygon_mesh_processing/remove_degeneracies_example_OM.cpp \example Polygon_mesh_processing/manifoldness_repair_example.cpp */ diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index 15092e7a30f..2a3bc719826 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -89,7 +89,7 @@ create_single_source_cgal_program( "face_filtered_graph_example.cpp") create_single_source_cgal_program( "orient_polygon_soup_example.cpp") create_single_source_cgal_program( "triangulate_polyline_example.cpp") create_single_source_cgal_program( "mesh_slicer_example.cpp") -create_single_source_cgal_program( "remove_degeneracies_example.cpp") +#create_single_source_cgal_program( "remove_degeneracies_example.cpp") create_single_source_cgal_program( "isotropic_remeshing_example.cpp") create_single_source_cgal_program( "isotropic_remeshing_of_patch_example.cpp") create_single_source_cgal_program( "surface_mesh_intersection.cpp") @@ -119,8 +119,8 @@ target_link_libraries( point_inside_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) create_single_source_cgal_program( "stitch_borders_example_OM.cpp" ) target_link_libraries( stitch_borders_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) -create_single_source_cgal_program( "remove_degeneracies_example_OM.cpp") -target_link_libraries( remove_degeneracies_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) +#create_single_source_cgal_program( "remove_degeneracies_example_OM.cpp") +#target_link_libraries( remove_degeneracies_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) create_single_source_cgal_program( "triangulate_faces_example_OM.cpp") target_link_libraries( triangulate_faces_example_OM PRIVATE ${OPENMESH_LIBRARIES} ) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp deleted file mode 100644 index c79f9a5337f..00000000000 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include - -#include - -#include - -#include -#include - -typedef CGAL::Exact_predicates_inexact_constructions_kernel K; - -typedef CGAL::Surface_mesh Surface_mesh; - -int main(int argc, char* argv[]) -{ - const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; - std::ifstream input(filename); - - Surface_mesh mesh; - if (!input || !(input >> mesh) || mesh.is_empty()) { - std::cerr << "Not a valid .off file." << std::endl; - return EXIT_FAILURE; - } - - std::size_t nb = CGAL::Polygon_mesh_processing::remove_degenerate_faces(mesh); - - std::cout << "There were " << nb << " degenerate faces in this mesh" << std::endl; - - mesh.collect_garbage(); - std::ofstream out("repaired.off"); - out << mesh; - - return EXIT_SUCCESS; -} diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp deleted file mode 100644 index 46763164848..00000000000 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include - -#include -#include - -#include -#include - -#include -#include - -typedef CGAL::Exact_predicates_inexact_constructions_kernel K; - -typedef OpenMesh::PolyMesh_ArrayKernelT< > Mesh; - -namespace PMP = CGAL::Polygon_mesh_processing; - -int main(int argc, char* argv[]) -{ - const char* filename = (argc > 1) ? argv[1] : "data/degtri_sliding.off"; - - Mesh mesh; - if (!OpenMesh::IO::read_mesh(mesh, filename) || num_vertices(mesh)) { - std::cerr << "Not a valid .off file." << std::endl; - return EXIT_FAILURE; - } - - std::size_t nb = PMP::remove_degenerate_faces(mesh, - CGAL::parameters::vertex_point_map(get(CGAL::vertex_point, mesh)) - .geom_traits(K())); - - std::cout << "There were " << nb << " degenerate faces in this mesh" << std::endl; - - mesh.garbage_collection(); - OpenMesh::IO::write_mesh(mesh, "repaired.off"); - - return EXIT_SUCCESS; -} diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 88a12acec66..a5af3737c07 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -271,7 +271,6 @@ degenerate_edges(const TriangleMesh& tm, OutputIterator out) /// \cgalParamEnd /// \cgalNamedParamsEnd /// -/// \sa remove_degenerate_faces() template OutputIterator degenerate_faces(const FaceRange& faces, const TriangleMesh& tm, @@ -792,40 +791,40 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, return remove_degenerate_edges(edge_range, tmesh, parameters::all_default()); } -/// \ingroup PMP_repairing_grp -/// removes the degenerate faces from a triangulated surface mesh. -/// A face is considered degenerate if two of its vertices share the same location, -/// or more generally if all its vertices are collinear. -/// -/// @pre `CGAL::is_triangle_mesh(tmesh)` -/// -/// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` -/// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" -/// -/// @param tmesh the triangulated surface mesh to be repaired -/// @param np optional \ref pmp_namedparameters "Named Parameters" described below -/// -/// \cgalNamedParamsBegin -/// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. -/// The type of this map is model of `ReadWritePropertyMap`. -/// If this parameter is omitted, an internal property map for -/// `CGAL::vertex_point_t` must be available in `TriangleMesh` -/// \cgalParamEnd -/// \cgalParamBegin{geom_traits} a geometric traits class instance. -/// The traits class must provide the nested type `Point_3`, -/// and the nested functors : -/// - `Compare_distance_3` to compute the distance between 2 points -/// - `Collinear_3` to check whether 3 points are collinear -/// - `Less_xyz_3` to compare lexicographically two points -/// - `Equal_3` to check whether 2 points are identical -/// For each functor `Foo`, a function `Foo foo_object()` -/// \cgalParamEnd -/// \cgalNamedParamsEnd -/// -/// \todo the function might not be able to remove all degenerate faces. -/// We should probably do something with the return type. -/// -/// \return number of removed degenerate faces +// \ingroup PMP_repairing_grp +// removes the degenerate faces from a triangulated surface mesh. +// A face is considered degenerate if two of its vertices share the same location, +// or more generally if all its vertices are collinear. +// +// @pre `CGAL::is_triangle_mesh(tmesh)` +// +// @tparam TriangleMesh a model of `FaceListGraph` and `MutableFaceGraph` +// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" +// +// @param tmesh the triangulated surface mesh to be repaired +// @param np optional \ref pmp_namedparameters "Named Parameters" described below +// +// \cgalNamedParamsBegin +// \cgalParamBegin{vertex_point_map} the property map with the points associated to the vertices of `pmesh`. +// The type of this map is model of `ReadWritePropertyMap`. +// If this parameter is omitted, an internal property map for +// `CGAL::vertex_point_t` must be available in `TriangleMesh` +// \cgalParamEnd +// \cgalParamBegin{geom_traits} a geometric traits class instance. +// The traits class must provide the nested type `Point_3`, +// and the nested functors : +// - `Compare_distance_3` to compute the distance between 2 points +// - `Collinear_3` to check whether 3 points are collinear +// - `Less_xyz_3` to compare lexicographically two points +// - `Equal_3` to check whether 2 points are identical +// For each functor `Foo`, a function `Foo foo_object()` +// \cgalParamEnd +// \cgalNamedParamsEnd +// +// \todo the function might not be able to remove all degenerate faces. +// We should probably do something with the return type. +// +// \return number of removed degenerate faces template std::size_t remove_degenerate_faces(TriangleMesh& tmesh, const NamedParameters& np) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 4cc98184b7f..35e2a7a0920 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -117,7 +117,6 @@ bool is_degenerate_edge(typename boost::graph_traits::edge_descript /// \cgalNamedParamsEnd /// /// \sa `degenerate_faces()` -/// \sa `remove_degenerate_faces()` /// /// \return `true` if the face `f` is degenerate, `false` otherwise. template diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp index c8b2349fe1c..d286ef3f50f 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Repair_polyhedron_plugin.cpp @@ -54,7 +54,7 @@ public: actionAutorefine->setObjectName("actionAutorefine"); actionAutorefineAndRMSelfIntersections->setObjectName("actionAutorefineAndRMSelfIntersections"); - actionRemoveDegenerateFaces->setProperty("subMenuName", "Polygon Mesh Processing/Repair"); + actionRemoveDegenerateFaces->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionStitchCloseBorderHalfedges->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionRemoveSelfIntersections->setProperty("subMenuName", "Polygon Mesh Processing/Repair/Experimental"); actionRemoveIsolatedVertices->setProperty("subMenuName", "Polygon Mesh Processing/Repair"); From a4d825f144a505af3daac842e3b67b8920235c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 11:29:25 +0200 Subject: [PATCH 49/65] Misc minor changes --- Polygon_mesh_processing/dont_submit | 2 -- .../CGAL/Polygon_mesh_processing/merge_border_vertices.h | 2 ++ .../include/CGAL/Polygon_mesh_processing/repair.h | 2 +- .../include/CGAL/Polygon_mesh_processing/shape_predicates.h | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 Polygon_mesh_processing/dont_submit diff --git a/Polygon_mesh_processing/dont_submit b/Polygon_mesh_processing/dont_submit deleted file mode 100644 index 9ee49f9ce2d..00000000000 --- a/Polygon_mesh_processing/dont_submit +++ /dev/null @@ -1,2 +0,0 @@ -examples/Polygon_mesh_processing/remove_degeneracies_example.cpp -examples/Polygon_mesh_processing/remove_degeneracies_example_OM.cpp diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index 07fe1728d46..ad0622a1b62 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -22,6 +22,8 @@ #ifndef CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H #define CGAL_POLYGON_MESH_PROCESSING_MERGE_BORDER_VERTICES_H +#include + #include #include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index a5af3737c07..1927ee1b57c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -175,7 +175,7 @@ struct Less_vertex_point{ /// collects the degenerate edges within a given range of edges. /// /// @tparam EdgeRange a model of `Range` with value type `boost::graph_traits::%edge_descriptor` -/// @tparam TriangleMesh a model of `EdgeListGraph` +/// @tparam TriangleMesh a model of `HalfedgeGraph` /// @tparam NamedParameters a sequence of \ref pmp_namedparameters "Named Parameters" /// /// @param edges a subset of edges of `tm` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 35e2a7a0920..0057c32e0da 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -23,6 +23,8 @@ #ifndef CGAL_POLYGON_MESH_PROCESSING_SHAPE_PREDICATES_H #define CGAL_POLYGON_MESH_PROCESSING_SHAPE_PREDICATES_H +#include + #include #include From 789d416f2142b42ff8774c2f04c72648ac8371f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 25 Jul 2018 11:33:44 +0200 Subject: [PATCH 50/65] Moved 'merge_vertices_in_range' to internal namespace and undocumented it --- .../PackageDescription.txt | 1 - .../merge_border_vertices.h | 26 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index d937e76949b..e2a59d63cf9 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -134,7 +134,6 @@ and provides a list of the parameters that are used in this package. - `CGAL::Polygon_mesh_processing::remove_isolated_vertices()` - `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()` - `CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices()` -- `CGAL::Polygon_mesh_processing::merge_vertices_in_range()` - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()` - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles()` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index ad0622a1b62..da456fbf214 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -138,18 +138,16 @@ void detect_identical_mergeable_vertices( } } -} // end of internal - -/// \ingroup PMP_repairing_grp -/// merges target vertices of a list of halfedges. -/// Halfedges must be sorted in the list. -/// -/// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. -/// @tparam HalfedgeRange a range of halfedge descriptors of `PolygonMesh`, model of `Range`. -/// -/// @param sorted_hedges a sorted list of halfedges. -/// @param pm the polygon mesh which contains the list of halfedges. -/// +// \ingroup PMP_repairing_grp +// merges target vertices of a list of halfedges. +// Halfedges must be sorted in the list. +// +// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. +// @tparam HalfedgeRange a range of halfedge descriptors of `PolygonMesh`, model of `Range`. +// +// @param sorted_hedges a sorted list of halfedges. +// @param pm the polygon mesh which contains the list of halfedges. +// template void merge_vertices_in_range(const HalfedgeRange& sorted_hedges, PolygonMesh& pm) @@ -192,6 +190,8 @@ void merge_vertices_in_range(const HalfedgeRange& sorted_hedges, remove_vertex(vd, pm); } +} // end of internal + /// \ingroup PMP_repairing_grp /// merges identical vertices around a cycle of connected edges. /// @@ -239,7 +239,7 @@ void merge_duplicated_vertices_in_boundary_cycle( { start=hedges.front(); // hedges are sorted along the cycle - merge_vertices_in_range(hedges, pm); + internal::merge_vertices_in_range(hedges, pm); } } From 1765ae106bee9243f05dafbbd940002b61ae74e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Thu, 26 Jul 2018 15:49:48 +0200 Subject: [PATCH 51/65] Added new headers to pmp.h --- Polygon_mesh_processing/include/CGAL/polygon_mesh_processing.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/polygon_mesh_processing.h b/Polygon_mesh_processing/include/CGAL/polygon_mesh_processing.h index 7d587ad4b84..164b29b25b7 100644 --- a/Polygon_mesh_processing/include/CGAL/polygon_mesh_processing.h +++ b/Polygon_mesh_processing/include/CGAL/polygon_mesh_processing.h @@ -49,6 +49,8 @@ #include #include #include +#include +#include // the named parameter header being not documented the doc is put here for now #ifdef DOXYGEN_RUNNING From f58247d8dfe2c311ab91a5068c33e1077fdb7911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 30 Jul 2018 15:48:20 +0200 Subject: [PATCH 52/65] Added missing quotes --- .../include/CGAL/Polygon_mesh_processing/repair.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 1927ee1b57c..328f4bb3d15 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -1467,7 +1467,7 @@ struct Vertex_collector /// @param v a vertex of `tm` /// @param tm a triangle mesh containing `v` /// -/// \sa duplicate_non_manifold_vertices() +/// \sa `duplicate_non_manifold_vertices()` /// /// \return `true` if the vertex is non-manifold, `false` otherwise. template From 9752621e7a9cac826a7cd9a835e24b53d932502f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 30 Jul 2018 15:50:23 +0200 Subject: [PATCH 53/65] Updated a figure --- .../fig/selfintersections.jpg | Bin 44618 -> 190744 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/fig/selfintersections.jpg b/Polygon_mesh_processing/doc/Polygon_mesh_processing/fig/selfintersections.jpg index 7504ff7b8650e0a1299c505998d1fad58cdb8e90..ffc0bc0cfc24e8b0e5b9fdfd08c721a597e97eb2 100644 GIT binary patch literal 190744 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 rni>TunJ literal 44618 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

h_M#x(;X*xtYkz->T963CZAgKK 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 Date: Thu, 9 Aug 2018 08:52:13 +0200 Subject: [PATCH 54/65] Removed useless 'typename' --- .../test_shape_predicates.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp index e68e8a7d09f..8f76f46fae5 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/test_shape_predicates.cpp @@ -19,7 +19,7 @@ void check_edge_degeneracy(const char* fname) { std::cout << "test edge degeneracy..."; - typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + typedef boost::graph_traits::edge_descriptor edge_descriptor; std::ifstream input(fname); Surface_mesh mesh; @@ -39,7 +39,7 @@ void check_triangle_face_degeneracy(const char* fname) { std::cout << "test face degeneracy..."; - typedef typename boost::graph_traits::face_descriptor face_descriptor; + typedef boost::graph_traits::face_descriptor face_descriptor; std::ifstream input(fname); Surface_mesh mesh; @@ -82,8 +82,8 @@ void test_vertex_non_manifoldness(const char* fname) { std::cout << "test vertex non manifoldness..."; - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - typedef typename boost::graph_traits::vertices_size_type size_type; + typedef boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef boost::graph_traits::vertices_size_type size_type; std::ifstream input(fname); Surface_mesh mesh; @@ -117,7 +117,7 @@ void test_vertices_merge_and_duplication(const char* fname) { std::cout << "test non manifold vertex duplication..."; - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef boost::graph_traits::vertex_descriptor vertex_descriptor; std::ifstream input(fname); Surface_mesh mesh; @@ -166,9 +166,9 @@ void test_needles_and_caps(const char* fname) namespace PMP = CGAL::Polygon_mesh_processing; - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - typedef typename boost::graph_traits::face_iterator face_iterator; - typedef typename boost::graph_traits::face_descriptor face_descriptor; + typedef boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef boost::graph_traits::face_iterator face_iterator; + typedef boost::graph_traits::face_descriptor face_descriptor; std::ifstream input(fname); Surface_mesh mesh; From 86174e394f387f2e839fe93ccc0d27522b738e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Thu, 9 Aug 2018 08:52:23 +0200 Subject: [PATCH 55/65] Added missing header --- Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h index 155313f10a0..d0449e819b5 100644 --- a/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h +++ b/Polyhedron/demo/Polyhedron/include/CGAL/statistics_helpers.h @@ -1,13 +1,15 @@ #ifndef POLYHEDRON_DEMO_STATISTICS_HELPERS_H #define POLYHEDRON_DEMO_STATISTICS_HELPERS_H +#include +#include + #include #include #include #include #include #include -#include #include #include From f5025a697f4d8c0652e0dc762049dd104784b7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Fri, 10 Aug 2018 08:55:29 +0200 Subject: [PATCH 56/65] Removed useless typedefs --- .../demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp index 929890a4d8b..ddbdfc8c751 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Degenerated_faces_plugin.cpp @@ -83,9 +83,7 @@ template bool isDegen(Mesh* mesh, std::vector::face_descriptor> &out_faces) { typedef typename boost::graph_traits::face_descriptor FaceDescriptor; - typedef typename boost::property_map::type Vpm; - typedef typename boost::property_traits::value_type Point; - typedef typename CGAL::Kernel_traits::Kernel Kernel; + //filter non-triangle_faces BOOST_FOREACH(FaceDescriptor f, faces(*mesh)) { From 89df0d977f252be3e3179d1e59ac3b76efffeed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 16 Aug 2018 16:55:49 +0200 Subject: [PATCH 57/65] typo and indicate that the cycle is a boundary cycle --- .../CGAL/Polygon_mesh_processing/merge_border_vertices.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h index da456fbf214..d4600321959 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/merge_border_vertices.h @@ -193,13 +193,13 @@ void merge_vertices_in_range(const HalfedgeRange& sorted_hedges, } // end of internal /// \ingroup PMP_repairing_grp -/// merges identical vertices around a cycle of connected edges. +/// merges identical vertices around a cycle of boundary edges. /// /// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. /// @tparam NamedParameter a sequence of \ref pmp_namedparameters "Named Parameters". /// -/// @param h a halfedge that belongs to the cycle. -/// @param pm the polygon mesh which containts the cycle. +/// @param h a halfedge that belongs to a boundary cycle. +/// @param pm the polygon mesh which contains the boundary cycle. /// @param np optional parameter of \ref pmp_namedparameters "Named Parameters" listed below. /// /// \cgalNamedParamsBegin @@ -249,7 +249,7 @@ void merge_duplicated_vertices_in_boundary_cycle( /// @tparam PolygonMesh a model of `FaceListGraph` and `MutableFaceGraph`. /// @tparam NamedParameter a sequence of \ref pmp_namedparameters "Named Parameters". /// -/// @param pm the polygon mesh which containts the cycle. +/// @param pm the polygon mesh which contains the cycles. /// @param np optional parameter of \ref pmp_namedparameters "Named Parameters" listed below. /// /// \cgalNamedParamsBegin From 7b740e9561567b5e91605a30c70fddf8c5505856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Fri, 17 Aug 2018 10:44:35 +0200 Subject: [PATCH 58/65] Fixed 'is_non_manifold_vertex' A pinched vertex is not manifold --- .../CGAL/Polygon_mesh_processing/repair.h | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 328f4bb3d15..f54546f528a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -1460,36 +1460,47 @@ struct Vertex_collector } // end namespace internal /// \ingroup PMP_repairing_grp -/// checks whether a vertex of a triangle mesh is non-manifold. +/// checks whether a vertex of a polygon mesh is non-manifold. /// -/// @tparam TriangleMesh a model of `HalfedgeListGraph` +/// @tparam PolygonMesh a model of `HalfedgeListGraph` /// -/// @param v a vertex of `tm` -/// @param tm a triangle mesh containing `v` +/// @param v a vertex of `pm` +/// @param pm a triangle mesh containing `v` /// /// \sa `duplicate_non_manifold_vertices()` /// /// \return `true` if the vertex is non-manifold, `false` otherwise. -template -bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, - const TriangleMesh& tm) +template +bool is_non_manifold_vertex(typename boost::graph_traits::vertex_descriptor v, + const PolygonMesh& pm) { - CGAL_assertion(CGAL::is_triangle_mesh(tm)); - - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; boost::unordered_set halfedges_handled; - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, tm)) - halfedges_handled.insert(h); - BOOST_FOREACH(halfedge_descriptor h, halfedges(tm)) + std::size_t incident_null_faces_counter = 0; + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(v, pm)) { - if(v == target(h, tm)) + halfedges_handled.insert(h); + if(CGAL::is_border(h, pm)) + ++incident_null_faces_counter; + } + + if(incident_null_faces_counter > 1) + { + // The vertex is the sole connection between two connected components --> non-manifold + return true; + } + + BOOST_FOREACH(halfedge_descriptor h, halfedges(pm)) + { + if(v == target(h, pm)) { + // More than one umbrella incident to 'v' --> non-manifold if(halfedges_handled.count(h) == 0) return true; } } + return false; } From 979456be47e364ca2c7400619e58d8e64f25b4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Fri, 17 Aug 2018 16:32:55 +0200 Subject: [PATCH 59/65] Fixed typo --- .../include/CGAL/Polygon_mesh_processing/repair.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index f54546f528a..780e983b508 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -1521,7 +1521,7 @@ bool is_non_manifold_vertex(typename boost::graph_traits::vertex_de /// \cgalParamEnd /// \cgalParamBegin{vertex_is_constrained_map} a writable property map with `vertex_descriptor` /// as key and `bool` as `value_type`. `put(pmap, v, true)` will be called for each duplicated -/// vertices, as well as the original non-manifold vertex in the input mehs. +/// vertices, as well as the original non-manifold vertex in the input mesh. /// \cgalParamEnd /// \cgalParamBegin{output_iterator} a model of `OutputIterator` with value type /// `std::vector`. The first vertex of each vector is a non-manifold vertex From d1f969ec86ec28b0caa9ada6f4d1d3c5b6430448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 28 Aug 2018 11:46:20 +0200 Subject: [PATCH 60/65] Changed CHANGES.md --- Installation/CHANGES.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Installation/CHANGES.md b/Installation/CHANGES.md index 36ec3ab0f90..98c17855417 100644 --- a/Installation/CHANGES.md +++ b/Installation/CHANGES.md @@ -1,6 +1,25 @@ Release History =============== +Release 4.14 +------------ + +Release date: March 2019 + +### Polygon Mesh Processing package +- Added the following new functions to detect and repair mesh degeneracies: + - `CGAL::Polygon_mesh_processing::degenerate_edges()` + - `CGAL::Polygon_mesh_processing::degenerate_faces()` + - `CGAL::Polygon_mesh_processing::is_non_manifold_vertex()` + - `CGAL::Polygon_mesh_processing::is_degenerate_triangle_face()` + - `CGAL::Polygon_mesh_processing::is_degenerate_edge()` + - `CGAL::Polygon_mesh_processing::is_needle_triangle_face()` + - `CGAL::Polygon_mesh_processing::is_cap_triangle_face()` + - `CGAL::Polygon_mesh_processing::duplicate_non_manifold_vertices()` + - `CGAL::Polygon_mesh_processing::extract_boundary_cycles()` + - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycle()` + - `CGAL::Polygon_mesh_processing::merge_duplicated_vertices_in_boundary_cycles()` + Release 4.13 ------------ From 72422ca498798ec8cfccfe2064088e8b5173d5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Tue, 28 Aug 2018 11:46:34 +0200 Subject: [PATCH 61/65] Replaced ::max() (to avoid issues with NTs that do not have a max value) --- .../shape_predicates.h | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 0057c32e0da..7b4a84e36cf 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -185,7 +185,6 @@ is_needle_triangle_face(typename boost::graph_traits::face_descrip const double threshold, const NamedParameters& np) { - CGAL_precondition(CGAL::is_triangle_mesh(tm)); CGAL_precondition(threshold >= 1.); using boost::get_param; @@ -202,15 +201,22 @@ is_needle_triangle_face(typename boost::graph_traits::face_descrip typedef typename Traits::FT FT; - const halfedge_descriptor h0 = halfedge(f, tm); - FT max_sq_length = - std::numeric_limits::max(), - min_sq_length = std::numeric_limits::max(); - halfedge_descriptor min_h = boost::graph_traits::null_halfedge(); + CGAL::Halfedge_around_face_iterator hit, hend; + boost::tie(hit, hend) = CGAL::halfedges_around_face(halfedge(f, tm), tm); + CGAL_precondition(std::distance(hit, hend) == 3); - BOOST_FOREACH(halfedge_descriptor h, halfedges_around_face(h0, tm)) + const halfedge_descriptor h0 = *hit++; + FT sq_length = traits.compute_squared_distance_3_object()(get(vpmap, source(h0, tm)), + get(vpmap, target(h0, tm))); + + FT min_sq_length = sq_length, max_sq_length = sq_length; + halfedge_descriptor min_h = h0; + + for(; hit!=hend; ++hit) { - const FT sq_length = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), - get(vpmap, target(h, tm))); + const halfedge_descriptor h = *hit; + sq_length = traits.compute_squared_distance_3_object()(get(vpmap, source(h, tm)), + get(vpmap, target(h, tm))); if(max_sq_length < sq_length) max_sq_length = sq_length; From 8cb8102cfc3349c03284df640bfb0df869ce516a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Mon, 3 Sep 2018 16:35:02 +0200 Subject: [PATCH 62/65] Fixed not incrementing index --- .../include/CGAL/Polygon_mesh_processing/shape_predicates.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index 7b4a84e36cf..a84f99aae69 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -338,6 +338,8 @@ is_cap_triangle_face(typename boost::graph_traits::face_descriptor if(neg_sp && sq_cos >= sq_threshold) return prev(h, tm); + + ++pos; } return boost::graph_traits::null_halfedge(); } From 6a885796bc793360e2ce16d45cb6622a7bc9a59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 6 Sep 2018 16:44:53 +0200 Subject: [PATCH 63/65] do not test the whole mesh, only the current face --- .../include/CGAL/Polygon_mesh_processing/shape_predicates.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h index a84f99aae69..c1f0b7c040b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/shape_predicates.h @@ -126,7 +126,7 @@ bool is_degenerate_triangle_face(typename boost::graph_traits::fac const TriangleMesh& tm, const NamedParameters& np) { - CGAL_precondition(CGAL::is_triangle_mesh(tm)); + CGAL_precondition(CGAL::is_triangle(halfedge(f, tm), tm)); using boost::get_param; using boost::choose_param; From e4ad5d96a7afd1f2cca3fbc7cb2661a1c1be7659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 6 Sep 2018 16:46:01 +0200 Subject: [PATCH 64/65] start adding support for open meshes --- .../CGAL/Polygon_mesh_processing/repair.h | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 780e983b508..58c064e1bb3 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -502,7 +502,7 @@ remove_a_border_edge(typename boost::graph_traits::edge_descriptor } template -std::size_t remove_degenerate_edges(const EdgeRange& edge_range, +bool remove_degenerate_edges(const EdgeRange& edge_range, TriangleMesh& tmesh, const NamedParameters& np) { @@ -525,6 +525,7 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, typedef typename GetGeomTraits::type Traits; std::size_t nb_deg_faces = 0; + bool all_removed=true; // collect edges of length 0 std::set degenerate_edges_to_remove; @@ -572,6 +573,36 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, continue; } } + else + { + halfedge_descriptor hd = halfedge(ed,tmesh); + // if both vertices are boundary vertices we can't do anything + bool impossible = false; + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_target(hd, tmesh)) + { + if (is_border(h, tmesh)) + { + impossible = true; + break; + } + } + if (impossible) + { + BOOST_FOREACH(halfedge_descriptor h, halfedges_around_source(hd, tmesh)) + { + if (is_border(h, tmesh)) + { + impossible = true; + break; + } + } + if (impossible) + { + all_removed=false; + continue; + } + } + } // When the edge does not satisfy the link condition, it means that it cannot be // collapsed as is. In the following we assume that there is no topological issue @@ -781,11 +812,11 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, } } - return nb_deg_faces; + return all_removed; } template -std::size_t remove_degenerate_edges(const EdgeRange& edge_range, +bool remove_degenerate_edges(const EdgeRange& edge_range, TriangleMesh& tmesh) { return remove_degenerate_edges(edge_range, tmesh, parameters::all_default()); @@ -824,10 +855,10 @@ std::size_t remove_degenerate_edges(const EdgeRange& edge_range, // \todo the function might not be able to remove all degenerate faces. // We should probably do something with the return type. // -// \return number of removed degenerate faces +/// \return `true` if all degenerate faces were successfully removed, and `false` otherwise. template -std::size_t remove_degenerate_faces(TriangleMesh& tmesh, - const NamedParameters& np) +bool remove_degenerate_faces( TriangleMesh& tmesh, + const NamedParameters& np) { CGAL_assertion(CGAL::is_triangle_mesh(tmesh)); @@ -851,7 +882,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, typedef typename boost::property_traits::reference Point_ref; // First remove edges of length 0 - std::size_t nb_deg_faces = remove_degenerate_edges(edges(tmesh), tmesh, np); + bool all_removed = remove_degenerate_edges(edges(tmesh), tmesh, np); #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG { @@ -865,8 +896,20 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, // Then, remove triangles made of 3 collinear points std::set degenerate_face_set; degenerate_faces(tmesh, std::inserter(degenerate_face_set, degenerate_face_set.begin()), np); - - nb_deg_faces+=degenerate_face_set.size(); +// Ignore faces with null edges + if (!all_removed) + { + BOOST_FOREACH(edge_descriptor ed, edges(tmesh)) + { + if ( traits.equal_3_object()(get(vpmap, target(ed, tmesh)), get(vpmap, source(ed, tmesh))) ) + { + halfedge_descriptor h = halfedge(ed, tmesh); + if (!is_border(h, tmesh)) degenerate_face_set.erase(face(h, tmesh)); + h=opposite(h, tmesh); + if (!is_border(h, tmesh)) degenerate_face_set.erase(face(h, tmesh)); + } + } + } // first remove degree 3 vertices that are part of a cap // (only the vertex in the middle of the opposite edge) @@ -879,7 +922,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, BOOST_FOREACH(halfedge_descriptor hd, halfedges_around_face(halfedge(fd, tmesh), tmesh)) { vertex_descriptor vd = target(hd, tmesh); - if (degree(vd, tmesh) == 3) + if (degree(vd, tmesh) == 3 && is_border(vd, tmesh)==GT::null_halfedge()) { vertices_to_remove.insert(vd); break; @@ -987,15 +1030,17 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, { Euler::flip_edge(edge_to_flip, tmesh); } + else + { + all_removed=false; #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG - else{ std::cout << " WARNING: flip is not possible\n"; // \todo Let p and q be the vertices opposite to `edge_to_flip`, and let // r be the vertex of `edge_to_flip` that is the furthest away from // the edge `pq`. In that case I think we should remove all the triangles // so that the triangle pqr is in the mesh. - } #endif + } } } else @@ -1410,7 +1455,7 @@ std::size_t remove_degenerate_faces(TriangleMesh& tmesh, } } - return nb_deg_faces; + return all_removed; } template From 7017a26d35a9194b33affc5ce259310519c63ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 6 Sep 2018 16:48:25 +0200 Subject: [PATCH 65/65] update conditions --- .../CGAL/Polygon_mesh_processing/repair.h | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h index 58c064e1bb3..917eac4b22e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/repair.h @@ -42,7 +42,7 @@ #include #include -#include +#include #ifdef CGAL_PMP_REMOVE_DEGENERATE_FACES_DEBUG #include @@ -214,8 +214,8 @@ template OutputIterator degenerate_edges(const EdgeRange& edges, const TriangleMesh& tm, OutputIterator out, - typename boost::disable_if_c< - CGAL::is_iterator::value + typename boost::enable_if< + typename boost::has_range_iterator >::type* = 0) { return degenerate_edges(edges, tm, out, CGAL::parameters::all_default()); @@ -231,9 +231,9 @@ OutputIterator degenerate_edges(const TriangleMesh& tm, OutputIterator out, const NamedParameters& np #ifndef DOXYGEN_RUNNING - , typename boost::enable_if_c< - CGAL::is_iterator::value - >::type* = 0 + , typename boost::disable_if< + boost::has_range_iterator + >::type* = 0 #endif ) { @@ -291,9 +291,9 @@ template OutputIterator degenerate_faces(const FaceRange& faces, const TriangleMesh& tm, OutputIterator out, - typename boost::disable_if_c< - CGAL::is_iterator::value - >::type* = 0) + typename boost::enable_if< + boost::has_range_iterator + >::type* = 0) { return degenerate_faces(faces, tm, out, CGAL::parameters::all_default()); } @@ -308,9 +308,9 @@ OutputIterator degenerate_faces(const TriangleMesh& tm, OutputIterator out, const NamedParameters& np #ifndef DOXYGEN_RUNNING - , typename boost::enable_if_c< - CGAL::is_iterator::value - >::type* = 0 + , typename boost::disable_if< + boost::has_range_iterator + >::type* = 0 #endif ) {