From 0ee9406235728660ecc74122fc20505bacfd21ba Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Thu, 30 Jul 2020 15:42:41 +0200 Subject: [PATCH 001/124] add possibility to provide a variable sizing field to PMP::isotropic_remeshing # Conflicts: # Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt # Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h --- .../Polygon_mesh_processing/CMakeLists.txt | 1 + ...sotropic_remeshing_with_sizing_example.cpp | 39 ++++++++ .../Isotropic_remeshing/Sizing_field.h | 50 ++++++++++ .../Uniform_sizing_field.h | 92 +++++++++++++++++++ .../Isotropic_remeshing/remesh_impl.h | 60 ++++++------ .../CGAL/Polygon_mesh_processing/remesh.h | 32 +++++-- 6 files changed, 233 insertions(+), 41 deletions(-) create mode 100644 Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp create mode 100644 Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h create mode 100644 Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index c755ecf63e8e..cf1424b05c92 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -27,6 +27,7 @@ create_single_source_cgal_program("mesh_slicer_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("isotropic_remeshing_with_sizing_example.cpp") create_single_source_cgal_program("tangential_relaxation_example.cpp") create_single_source_cgal_program("surface_mesh_intersection.cpp") create_single_source_cgal_program("corefinement_SM.cpp") diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp new file mode 100644 index 000000000000..fa5dc55d7896 --- /dev/null +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef CGAL::Surface_mesh Mesh; + +namespace PMP = CGAL::Polygon_mesh_processing; + +int main(int argc, char* argv[]) +{ + const char* filename = (argc > 1) ? argv[1] : "data/pig.off"; + std::ifstream input(filename); + + Mesh mesh; + if (!input || !(input >> mesh) || !CGAL::is_triangle_mesh(mesh)) { + std::cerr << "Not a valid input file." << std::endl; + return 1; + } + + double target_edge_length = 0.04; + unsigned int nb_iter = 3; + + std::cout << "Start remeshing of " << filename + << " (" << num_faces(mesh) << " faces)..." << std::endl; + + PMP::isotropic_remeshing( + faces(mesh), + target_edge_length, + mesh, + PMP::parameters::number_of_iterations(nb_iter) + ); + + std::cout << "Remeshing done." << std::endl; + + return 0; +} diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h new file mode 100644 index 000000000000..3df70524feca --- /dev/null +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -0,0 +1,50 @@ +// Copyright (c) 2020 GeometryFactory (France) +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org) +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial +// +// +// Author(s) : Jane Tournois + +#ifndef CGAL_SIZING_FIELD_H +#define CGAL_SIZING_FIELD_H + +#include + +#include + +namespace CGAL +{ +/*! +* Sizing field virtual class +*/ +template +class Sizing_field +{ +private: + typedef PolygonMesh PM; + typedef typename boost::property_map::const_type VPMap; + typedef typename boost::property_traits::value_type Point; + typedef typename CGAL::Kernel_traits::Kernel K; + +public: + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef Point Point_3; + typedef typename K::FT FT; + +public: + virtual bool is_too_long(const halfedge_descriptor& h, FT& sql) const = 0; + virtual bool is_too_long(const vertex_descriptor& va, const vertex_descriptor& vb) const = 0; + virtual bool is_too_short(const halfedge_descriptor& h, FT& sqlen) const = 0; + virtual Point_3 split_placement(const halfedge_descriptor& h) const = 0; + +}; + +}//end namespace CGAL + +#endif //CGAL_SIZING_FIELD_H diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h new file mode 100644 index 000000000000..7b00635ff543 --- /dev/null +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -0,0 +1,92 @@ +// Copyright (c) 2020 GeometryFactory (France) +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org) +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial +// +// +// Author(s) : Jane Tournois + +#ifndef CGAL_UNIFORM_SIZING_FIELD_H +#define CGAL_UNIFORM_SIZING_FIELD_H + +#include + +#include + +#include + +namespace CGAL +{ +template +class Uniform_sizing_field : public CGAL::Sizing_field +{ +private: + typedef CGAL::Sizing_field Base; + +public: + typedef typename Base::FT FT; + typedef typename Base::Point_3 Point_3; + typedef typename Base::halfedge_descriptor halfedge_descriptor; + typedef typename Base::vertex_descriptor vertex_descriptor; + + Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + : m_sq_short( CGAL::square(4./5. * size)) + , m_sq_long( CGAL::square(4./3. * size)) + , m_pmesh(pmesh) + {} + +private: + FT sqlength(const vertex_descriptor& va, + const vertex_descriptor& vb) const + { + typename boost::property_map::const_type + vpmap = get(CGAL::vertex_point, m_pmesh); + return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); + } + + FT sqlength(const halfedge_descriptor& h) const + { + return sqlength(target(h, m_pmesh), source(h, m_pmesh)); + } + +public: + bool is_too_long(const halfedge_descriptor& h, FT& sqlen) const + { + sqlen = sqlength(h); + return sqlen > m_sq_long; + } + + bool is_too_long(const vertex_descriptor& va, + const vertex_descriptor& vb) const + { + FT sqlen = sqlength(va, vb); + return sqlen > m_sq_long; + } + + bool is_too_short(const halfedge_descriptor& h, FT& sqlen) const + { + sqlen = sqlength(h); + return sqlen < m_sq_short; + } + + virtual Point_3 split_placement(const halfedge_descriptor& h) const + { + typename boost::property_map::const_type + vpmap = get(CGAL::vertex_point, m_pmesh); + return CGAL::midpoint(get(vpmap, target(h, m_pmesh)), + get(vpmap, source(h, m_pmesh))); + } + +private: + FT m_sq_short; + FT m_sq_long; + const PolygonMesh& m_pmesh; +}; + +}//end namespace CGAL + +#endif //CGAL_UNIFORM_SIZING_FIELD_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 9cdb76ac76b0..e6911102a683 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 @@ -478,13 +478,12 @@ namespace internal { // "visits all edges of the mesh //if an edge is longer than the given threshold `high`, the edge //is split at its midpoint and the two adjacent triangles are bisected (2-4 split)" - void split_long_edges(const double& high) + template + void split_long_edges(const SizingFunction& sizing) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << "Split long edges (" << high << ")..." << std::endl; #endif - double sq_high = high*high; - //collect long edges typedef std::pair H_and_sql; std::multiset< H_and_sql, std::function > @@ -497,8 +496,8 @@ namespace internal { { if (!is_split_allowed(e)) continue; - double sqlen = sqlength(e); - if(sqlen > sq_high) + double sqlen; + if(sizing.is_too_long(halfedge(e, mesh_), sqlen)) long_edges.emplace(halfedge(e, mesh_), sqlen); } @@ -528,7 +527,7 @@ namespace internal { Patch_id patch_id_opp = get_patch_id(face(opposite(he, mesh_), mesh_)); //split edge - Point refinement_point = this->midpoint(he); + Point refinement_point = sizing.split_placement(he); halfedge_descriptor hnew = CGAL::Euler::split_edge(he, mesh_); CGAL_assertion(he == next(hnew, mesh_)); put(ecmap_, edge(hnew, mesh_), get(ecmap_, edge(he, mesh_)) ); @@ -548,13 +547,12 @@ namespace internal { halfedge_added(hnew_opp, status(opposite(he, mesh_))); //check sub-edges - double sqlen_new = 0.25 * sqlen; - if (sqlen_new > sq_high) - { - //if it was more than twice the "long" threshold, insert them - long_edges.emplace(hnew, sqlen_new); - long_edges.emplace(next(hnew, mesh_), sqlen_new); - } + //if it was more than twice the "long" threshold, insert them + double sqlen_new; + if(sizing.is_too_long(hnew, sqlen_new)) + long_edges.emplace(hnew, sqlen_new); + if(sizing.is_too_long(next(hnew, mesh_), sqlen_new)) + long_edges.insert(long_edge(next(hnew, mesh_), sqlen_new)); //insert new edges to keep triangular faces, and update long_edges if (!is_on_border(hnew)) @@ -573,8 +571,8 @@ namespace internal { if (snew == PATCH) { - double sql = sqlength(hnew2); - if (sql > sq_high) + double sql; + if(sizing.is_too_long(hnew2, sql)) long_edges.emplace(hnew2, sql); } } @@ -596,8 +594,8 @@ namespace internal { if (snew == PATCH) { - double sql = sqlength(hnew2); - if (sql > sq_high) + double sql; + if (sizing.is_too_long(hnew2, sql)) long_edges.emplace(hnew2, sql); } } @@ -620,8 +618,8 @@ namespace internal { // "collapses and thus removes all edges that are shorter than a // threshold `low`. [...] testing before each collapse whether the collapse // would produce an edge that is longer than `high`" - void collapse_short_edges(const double& low, - const double& high, + template + void collapse_short_edges(const SizingFunction& sizing, const bool collapse_constraints) { typedef boost::bimap< @@ -637,14 +635,13 @@ namespace internal { std::cout << "Fill bimap..."; std::cout.flush(); #endif - double sq_low = low*low; - double sq_high = high*high; Boost_bimap short_edges; for(edge_descriptor e : edges(mesh_)) { - double sqlen = sqlength(e); - if ((sqlen < sq_low) && is_collapse_allowed(e, collapse_constraints)) + double sqlen; + if( sizing.is_too_short(halfedge(e, mesh_), sqlen) + && is_collapse_allowed(e, collapse_constraints)) short_edges.insert(short_edge(halfedge(e, mesh_), sqlen)); } #ifdef CGAL_PMP_REMESHING_VERBOSE_PROGRESS @@ -741,7 +738,7 @@ namespace internal { for(halfedge_descriptor ha : halfedges_around_target(va, mesh_)) { vertex_descriptor va_i = source(ha, mesh_); - if (sqlength(vb, va_i) > sq_high) + if (sizing.is_too_long(vb, va_i)) { collapse_ok = false; break; @@ -796,7 +793,7 @@ namespace internal { //fix constrained case CGAL_assertion((is_constrained(vkept) || is_corner(vkept) || is_on_patch_border(vkept)) == (is_va_constrained || is_vb_constrained || is_va_on_constrained_polyline || is_vb_on_constrained_polyline)); - if (fix_degenerate_faces(vkept, short_edges, sq_low, collapse_constraints)) + if (fix_degenerate_faces(vkept, short_edges, sizing, collapse_constraints)) { #ifdef CGAL_PMP_REMESHING_DEBUG debug_status_map(); @@ -806,8 +803,9 @@ namespace internal { //insert new/remaining short edges for (halfedge_descriptor ht : halfedges_around_target(vkept, mesh_)) { - double sqlen = sqlength(ht); - if ((sqlen < sq_low) && is_collapse_allowed(edge(ht, mesh_), collapse_constraints)) + double sqlen; + if (sizing.is_too_short(ht, sqlen) + && is_collapse_allowed(edge(ht, mesh_), collapse_constraints)) short_edges.insert(short_edge(ht, sqlen)); } } @@ -1645,10 +1643,10 @@ namespace internal { // else keep current status for en and eno } - template + template bool fix_degenerate_faces(const vertex_descriptor& v, Bimap& short_edges, - const double& sq_low, + const SizingFunction& sizing, const bool collapse_constraints) { std::unordered_set degenerate_faces; @@ -1726,8 +1724,8 @@ namespace internal { //insert new edges in 'short_edges' if (is_collapse_allowed(edge(hf, mesh_), collapse_constraints)) { - double sqlen = sqlength(hf); - if (sqlen < sq_low) + double sqlen; + if (sizing.is_too_short(hf, sqlen)) short_edges.insert(typename Bimap::value_type(hf, sqlen)); } 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 6b37ec946ea9..791a0741a611 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -198,6 +199,21 @@ void isotropic_remeshing(const FaceRange& faces , const double& target_edge_length , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) +{ + isotropic_remeshing(faces, + CGAL::Uniform_sizing_field(target_edge_length, pmesh), + pmesh, + np); +} + +template +void isotropic_remeshing(const FaceRange& faces + , const SizingFunction& sizing + , PolygonMesh& pmesh + , const NamedParameters& np) { if (boost::begin(faces)==boost::end(faces)) return; @@ -261,12 +277,10 @@ void isotropic_remeshing(const FaceRange& faces #endif ) ) ); - double low = 4. / 5. * target_edge_length; - double high = 4. / 3. * target_edge_length; - #if !defined(CGAL_NO_PRECONDITIONS) if(protect) { + double high = 4. / 3. * target_edge_length; std::string msg("Isotropic remeshing : protect_constraints cannot be set to"); msg.append(" true with constraints larger than 4/3 * target_edge_length."); msg.append(" Remeshing aborted."); @@ -313,13 +327,11 @@ void isotropic_remeshing(const FaceRange& faces #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " * Iteration " << (i + 1) << " *" << std::endl; #endif - if (target_edge_length>0) - { - if(do_split) - remesher.split_long_edges(high); - if(do_collapse) - remesher.collapse_short_edges(low, high, collapse_constraints); - } + + if(do_split) + remesher.split_long_edges(sizing); + if(do_collapse) + remesher.collapse_short_edges(sizing, collapse_constraints); if(do_flip) remesher.flip_edges_for_valence_and_shape(); remesher.tangential_relaxation_impl(smoothing_1d, nb_laplacian); From 9de41310fd27bd8d55a7a9b75914f5c2d60f2a1f Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Thu, 30 Jul 2020 16:23:03 +0200 Subject: [PATCH 002/124] use boost::optional instead of a bool and a double # Conflicts: # Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/remesh_impl.h --- .../Isotropic_remeshing/Sizing_field.h | 14 ++--- .../Uniform_sizing_field.h | 35 ++++++++----- .../Isotropic_remeshing/remesh_impl.h | 51 ++++++++++--------- 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index 3df70524feca..fc6b14a984f5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -10,12 +10,13 @@ // // Author(s) : Jane Tournois -#ifndef CGAL_SIZING_FIELD_H -#define CGAL_SIZING_FIELD_H +#ifndef CGAL_PMP_REMESHING_SIZING_FIELD_H +#define CGAL_PMP_REMESHING_SIZING_FIELD_H #include #include +#include namespace CGAL { @@ -38,13 +39,14 @@ class Sizing_field typedef typename K::FT FT; public: - virtual bool is_too_long(const halfedge_descriptor& h, FT& sql) const = 0; - virtual bool is_too_long(const vertex_descriptor& va, const vertex_descriptor& vb) const = 0; - virtual bool is_too_short(const halfedge_descriptor& h, FT& sqlen) const = 0; + virtual boost::optional is_too_long(const halfedge_descriptor& h) const = 0; + virtual boost::optional is_too_long(const vertex_descriptor& va, + const vertex_descriptor& vb) const = 0; + virtual boost::optional is_too_short(const halfedge_descriptor& h) const = 0; virtual Point_3 split_placement(const halfedge_descriptor& h) const = 0; }; }//end namespace CGAL -#endif //CGAL_SIZING_FIELD_H +#endif //CGAL_PMP_REMESHING_SIZING_FIELD_H diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index 7b00635ff543..8704c84ef117 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -10,8 +10,8 @@ // // Author(s) : Jane Tournois -#ifndef CGAL_UNIFORM_SIZING_FIELD_H -#define CGAL_UNIFORM_SIZING_FIELD_H +#ifndef CGAL_PMP_REMESHING_UNIFORM_SIZING_FIELD_H +#define CGAL_PMP_REMESHING_UNIFORM_SIZING_FIELD_H #include @@ -54,23 +54,32 @@ class Uniform_sizing_field : public CGAL::Sizing_field } public: - bool is_too_long(const halfedge_descriptor& h, FT& sqlen) const + boost::optional is_too_long(const halfedge_descriptor& h) const { - sqlen = sqlength(h); - return sqlen > m_sq_long; + const FT sqlen = sqlength(h); + if(sqlen > m_sq_long) + return sqlen; + else + return boost::none; } - bool is_too_long(const vertex_descriptor& va, - const vertex_descriptor& vb) const + boost::optional is_too_long(const vertex_descriptor& va, + const vertex_descriptor& vb) const { - FT sqlen = sqlength(va, vb); - return sqlen > m_sq_long; + const FT sqlen = sqlength(va, vb); + if (sqlen > m_sq_long) + return sqlen; + else + return boost::none; } - bool is_too_short(const halfedge_descriptor& h, FT& sqlen) const + boost::optional is_too_short(const halfedge_descriptor& h) const { - sqlen = sqlength(h); - return sqlen < m_sq_short; + const FT sqlen = sqlength(h); + if (sqlen < m_sq_long) + return sqlen; + else + return boost::none; } virtual Point_3 split_placement(const halfedge_descriptor& h) const @@ -89,4 +98,4 @@ class Uniform_sizing_field : public CGAL::Sizing_field }//end namespace CGAL -#endif //CGAL_UNIFORM_SIZING_FIELD_H +#endif //CGAL_PMP_REMESHING_UNIFORM_SIZING_FIELD_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 e6911102a683..ed95352bab9c 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 @@ -496,9 +496,9 @@ namespace internal { { if (!is_split_allowed(e)) continue; - double sqlen; - if(sizing.is_too_long(halfedge(e, mesh_), sqlen)) - long_edges.emplace(halfedge(e, mesh_), sqlen); + boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_)); + if(sqlen != boost::none) + long_edges.emplace(halfedge(e, mesh_), sqlen.get()); } //split long edges @@ -548,11 +548,13 @@ namespace internal { //check sub-edges //if it was more than twice the "long" threshold, insert them - double sqlen_new; - if(sizing.is_too_long(hnew, sqlen_new)) - long_edges.emplace(hnew, sqlen_new); - if(sizing.is_too_long(next(hnew, mesh_), sqlen_new)) - long_edges.insert(long_edge(next(hnew, mesh_), sqlen_new)); + boost::optional sqlen_new = sizing.is_too_long(hnew); + if(sqlen_new != boost::none) + long_edges.emplace(hnew, sqlen_new.get()); + + sqlen_new = sizing.is_too_long(next(hnew, mesh_)); + if (sqlen_new != boost::none) + long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); //insert new edges to keep triangular faces, and update long_edges if (!is_on_border(hnew)) @@ -571,9 +573,9 @@ namespace internal { if (snew == PATCH) { - double sql; - if(sizing.is_too_long(hnew2, sql)) - long_edges.emplace(hnew2, sql); + boost::optional sql = sizing.is_too_long(hnew2); + if(sql != boost::none) + long_edges.emplace(hnew2, sql.get()); } } @@ -594,9 +596,9 @@ namespace internal { if (snew == PATCH) { - double sql; - if (sizing.is_too_long(hnew2, sql)) - long_edges.emplace(hnew2, sql); + boost::optional sql = sizing.is_too_long(hnew2); + if (sql != boost::none) + long_edges.emplace(hnew2, sql.get()); } } } @@ -639,10 +641,10 @@ namespace internal { Boost_bimap short_edges; for(edge_descriptor e : edges(mesh_)) { - double sqlen; - if( sizing.is_too_short(halfedge(e, mesh_), sqlen) + boost::optional sqlen = sizing.is_too_short(halfedge(e, mesh_)); + if(sqlen != boost::none && is_collapse_allowed(e, collapse_constraints)) - short_edges.insert(short_edge(halfedge(e, mesh_), sqlen)); + short_edges.insert(short_edge(halfedge(e, mesh_), sqlen.get())); } #ifdef CGAL_PMP_REMESHING_VERBOSE_PROGRESS std::cout << "done." << std::endl; @@ -738,7 +740,8 @@ namespace internal { for(halfedge_descriptor ha : halfedges_around_target(va, mesh_)) { vertex_descriptor va_i = source(ha, mesh_); - if (sizing.is_too_long(vb, va_i)) + boost::optional sqha = sizing.is_too_long(vb, va_i); + if (sqha != boost::none) { collapse_ok = false; break; @@ -803,10 +806,10 @@ namespace internal { //insert new/remaining short edges for (halfedge_descriptor ht : halfedges_around_target(vkept, mesh_)) { - double sqlen; - if (sizing.is_too_short(ht, sqlen) + boost::optional sqlen = sizing.is_too_short(ht); + if (sqlen != boost::none && is_collapse_allowed(edge(ht, mesh_), collapse_constraints)) - short_edges.insert(short_edge(ht, sqlen)); + short_edges.insert(short_edge(ht, sqlen.get())); } } }//end if(collapse_ok) @@ -1724,9 +1727,9 @@ namespace internal { //insert new edges in 'short_edges' if (is_collapse_allowed(edge(hf, mesh_), collapse_constraints)) { - double sqlen; - if (sizing.is_too_short(hf, sqlen)) - short_edges.insert(typename Bimap::value_type(hf, sqlen)); + boost::optional sqlen = sizing.is_too_short(hf); + if (sqlen != boost::none) + short_edges.insert(typename Bimap::value_type(hf, sqlen.get())); } if(!is_border(hf, mesh_) && From ad55b8cd9f7e6dd5cf9a54818c06570b36b861fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Sun, 11 Apr 2021 07:45:14 +0200 Subject: [PATCH 003/124] fix compilation issues --- .../Isotropic_remeshing/remesh_impl.h | 9 ++++----- .../CGAL/Polygon_mesh_processing/remesh.h | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) 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 ed95352bab9c..6103108cc50e 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 @@ -227,14 +227,14 @@ namespace internal { template + typename FacePatchMap, + typename SizingFunction> bool constraints_are_short_enough(const PM& pmesh, EdgeConstraintMap ecmap, VertexPointMap vpmap, const FacePatchMap& fpm, - const double& high) + const SizingFunction& sizing) { - double sqh = high*high; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::edge_descriptor edge_descriptor; for(edge_descriptor e : edges(pmesh)) @@ -244,8 +244,7 @@ namespace internal { get(ecmap, e) || get(fpm, face(h,pmesh))!=get(fpm, face(opposite(h,pmesh),pmesh)) ) { - if (sqh < CGAL::squared_distance(get(vpmap, source(h, pmesh)), - get(vpmap, target(h, pmesh)))) + if (sizing.is_too_long(source(h, pmesh), target(h, pmesh))) { return false; } 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 791a0741a611..8887a0b9beb5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -31,6 +31,16 @@ namespace CGAL { namespace Polygon_mesh_processing { +/*! \todo document me or merge the doc with the original overload*/ +template +void isotropic_remeshing(const FaceRange& faces + , const SizingFunction& sizing + , PolygonMesh& pmesh + , const NamedParameters& np); + /*! * \ingroup PMP_meshing_grp * @@ -200,8 +210,10 @@ void isotropic_remeshing(const FaceRange& faces , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { - isotropic_remeshing(faces, - CGAL::Uniform_sizing_field(target_edge_length, pmesh), + typedef Uniform_sizing_field Default_sizing; + isotropic_remeshing( + faces, + Default_sizing(target_edge_length, pmesh), pmesh, np); } @@ -280,12 +292,11 @@ void isotropic_remeshing(const FaceRange& faces #if !defined(CGAL_NO_PRECONDITIONS) if(protect) { - double high = 4. / 3. * target_edge_length; std::string msg("Isotropic remeshing : protect_constraints cannot be set to"); msg.append(" true with constraints larger than 4/3 * target_edge_length."); msg.append(" Remeshing aborted."); CGAL_precondition_msg( - internal::constraints_are_short_enough(pmesh, ecmap, vpmap, fpmap, high), + internal::constraints_are_short_enough(pmesh, ecmap, vpmap, fpmap, sizing), msg.c_str()); } #endif From 5c1e820c1eff9fba31e6622e7d4737cfcc619fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Sun, 11 Apr 2021 08:39:54 +0200 Subject: [PATCH 004/124] fix test and demo --- .../internal/Isotropic_remeshing/remesh_impl.h | 4 ++-- .../include/CGAL/Polygon_mesh_processing/remesh.h | 6 ++---- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) 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 6103108cc50e..f0daf9cc20a5 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 @@ -481,7 +481,7 @@ namespace internal { void split_long_edges(const SizingFunction& sizing) { #ifdef CGAL_PMP_REMESHING_VERBOSE - std::cout << "Split long edges (" << high << ")..." << std::endl; + std::cout << "Split long edges..." << std::endl; #endif //collect long edges typedef std::pair H_and_sql; @@ -629,7 +629,7 @@ namespace internal { typedef typename Boost_bimap::value_type short_edge; #ifdef CGAL_PMP_REMESHING_VERBOSE - std::cout << "Collapse short edges (" << low << ", " << high << ")..." + std::cout << "Collapse short edges..." << std::endl; #endif #ifdef CGAL_PMP_REMESHING_VERBOSE_PROGRESS 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 8887a0b9beb5..54c99b2de432 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -328,8 +328,7 @@ void isotropic_remeshing(const FaceRange& faces #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << std::endl; - std::cout << "Remeshing (size = " << target_edge_length; - std::cout << ", #iter = " << nb_iterations << ")..." << std::endl; + std::cout << "Remeshing (#iter = " << nb_iterations << ")..." << std::endl; t.reset(); t.start(); #endif @@ -355,8 +354,7 @@ void isotropic_remeshing(const FaceRange& faces #ifdef CGAL_PMP_REMESHING_VERBOSE t.stop(); - std::cout << "Remeshing done (size = " << target_edge_length; - std::cout << ", #iter = " << nb_iterations; + std::cout << "Remeshing done (#iter = " << nb_iterations; std::cout << ", " << t.time() << " sec )." << std::endl; #endif } diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 37dcedc8adb2..d20594ec1688 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -406,7 +406,7 @@ public Q_SLOTS: selection_item->constrained_edges_pmap(), get(CGAL::vertex_point, *selection_item->polyhedron()), CGAL::Constant_property_map(1), - 4. / 3. * target_length)) + CGAL::Uniform_sizing_field(target_length, pmesh))) { QApplication::restoreOverrideCursor(); //If facets are selected, splitting edges will add facets that won't be selected, and it will mess up the rest. @@ -606,7 +606,7 @@ public Q_SLOTS: ecm, get(CGAL::vertex_point, pmesh), CGAL::Constant_property_map(1), - 4. / 3. * target_length)) + CGAL::Uniform_sizing_field(target_length, pmesh))) { QApplication::restoreOverrideCursor(); QMessageBox::warning(mw, tr("Error"), From 50bbb4f6828ad0291cee8383c4decb8cb31b23a9 Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Mon, 12 Apr 2021 07:48:10 +0200 Subject: [PATCH 005/124] add namespace to avoid conflicts with Uniform_sizing_field in Mesh_3 --- .../internal/Isotropic_remeshing/Uniform_sizing_field.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index 8704c84ef117..3f32aacb1aa9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -21,6 +21,8 @@ namespace CGAL { +namespace Polygon_mesh_processing +{ template class Uniform_sizing_field : public CGAL::Sizing_field { @@ -96,6 +98,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field const PolygonMesh& m_pmesh; }; +}//end namespace Polygon_mesh_processing }//end namespace CGAL #endif //CGAL_PMP_REMESHING_UNIFORM_SIZING_FIELD_H From 13c4db20dd6f6858cad4eb8aa0b25b6d9b540739 Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Mon, 12 Apr 2021 11:28:32 +0200 Subject: [PATCH 006/124] fix max value for constraints length --- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index d20594ec1688..f1b138c16728 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -406,7 +406,7 @@ public Q_SLOTS: selection_item->constrained_edges_pmap(), get(CGAL::vertex_point, *selection_item->polyhedron()), CGAL::Constant_property_map(1), - CGAL::Uniform_sizing_field(target_length, pmesh))) + CGAL::Polygon_mesh_processing::Uniform_sizing_field( 4. / 3. * target_length, pmesh))) { QApplication::restoreOverrideCursor(); //If facets are selected, splitting edges will add facets that won't be selected, and it will mess up the rest. @@ -606,7 +606,7 @@ public Q_SLOTS: ecm, get(CGAL::vertex_point, pmesh), CGAL::Constant_property_map(1), - CGAL::Uniform_sizing_field(target_length, pmesh))) + CGAL::Polygon_mesh_processing::Uniform_sizing_field(4. / 3. * target_length, pmesh))) { QApplication::restoreOverrideCursor(); QMessageBox::warning(mw, tr("Error"), From 7326fb52ceea2c98462000cb6170e28ee1d8e0be Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 16 May 2023 22:56:41 +0200 Subject: [PATCH 007/124] Add initial preparations for adaptive sizing field Add Adaptive_sizing_field header with edge min and max limits, and tolerance Adjust the example --- ...sotropic_remeshing_with_sizing_example.cpp | 10 +- .../Adaptive_sizing_field.h | 112 ++++++++++++++++++ .../CGAL/Polygon_mesh_processing/remesh.h | 18 +++ 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index fa5dc55d7896..96325e6d8619 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -11,7 +11,7 @@ namespace PMP = CGAL::Polygon_mesh_processing; int main(int argc, char* argv[]) { - const char* filename = (argc > 1) ? argv[1] : "data/pig.off"; + const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/pig.off"); std::ifstream input(filename); Mesh mesh; @@ -20,7 +20,8 @@ int main(int argc, char* argv[]) return 1; } - double target_edge_length = 0.04; + const std::pair edge_min_max{0.1, 0.4}; + const double tol = 0.1; unsigned int nb_iter = 3; std::cout << "Start remeshing of " << filename @@ -28,11 +29,14 @@ int main(int argc, char* argv[]) PMP::isotropic_remeshing( faces(mesh), - target_edge_length, + edge_min_max, + tol, mesh, PMP::parameters::number_of_iterations(nb_iter) ); + CGAL::IO::write_polygon_mesh("out.off", mesh, CGAL::parameters::stream_precision(17)); + std::cout << "Remeshing done." << std::endl; return 0; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h new file mode 100644 index 000000000000..6b48181388e4 --- /dev/null +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -0,0 +1,112 @@ +// Copyright (c) 2020 GeometryFactory (France) +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org) +// +// $URL$ +// $Id$ +// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial +// +// +// Author(s) : Jane Tournois + +#ifndef CGAL_PMP_REMESHING_ADAPTIVE_SIZING_FIELD_H +#define CGAL_PMP_REMESHING_ADAPTIVE_SIZING_FIELD_H + +#include + +#include + +#include + +namespace CGAL +{ +namespace Polygon_mesh_processing +{ +template +class Adaptive_sizing_field : public CGAL::Sizing_field +{ +private: + typedef CGAL::Sizing_field Base; + +public: + typedef typename Base::FT FT; + typedef typename Base::Point_3 Point_3; + typedef typename Base::halfedge_descriptor halfedge_descriptor; + typedef typename Base::vertex_descriptor vertex_descriptor; + + Adaptive_sizing_field(const std::pair& edge_len_min_max + , const FT& tolerance + , const PolygonMesh& pmesh) + : m_sq_short( CGAL::square(edge_len_min_max.first)) + , m_sq_long( CGAL::square(edge_len_min_max.second)) + , tol(tolerance) + , m_pmesh(pmesh) + { + // calculate and store curvature and sizing field here in constructor? + // todo what about updating it? + } + +private: + FT sqlength(const vertex_descriptor& va, + const vertex_descriptor& vb) const + { + typename boost::property_map::const_type + vpmap = get(CGAL::vertex_point, m_pmesh); + return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); + } + + FT sqlength(const halfedge_descriptor& h) const + { + return sqlength(target(h, m_pmesh), source(h, m_pmesh)); + } + +public: + boost::optional is_too_long(const halfedge_descriptor& h) const + { + const FT sqlen = sqlength(h); + if(sqlen > m_sq_long) + return sqlen; + else + return boost::none; + } + + boost::optional is_too_long(const vertex_descriptor& va, + const vertex_descriptor& vb) const + { + const FT sqlen = sqlength(va, vb); + if (sqlen > m_sq_long) + return sqlen; + else + return boost::none; + } + + boost::optional is_too_short(const halfedge_descriptor& h) const + { + const FT sqlen = sqlength(h); + if (sqlen < m_sq_long) + return sqlen; + else + return boost::none; + } + + virtual Point_3 split_placement(const halfedge_descriptor& h) const + { + typename boost::property_map::const_type + vpmap = get(CGAL::vertex_point, m_pmesh); + return CGAL::midpoint(get(vpmap, target(h, m_pmesh)), + get(vpmap, source(h, m_pmesh))); + } + +private: + FT m_sq_short; + FT m_sq_long; + FT tol; + const PolygonMesh& m_pmesh; + //todo add property map containing sizing field form m_pmesh here +}; + +}//end namespace Polygon_mesh_processing +}//end namespace CGAL + +#endif //CGAL_PMP_REMESHING_ADAPTIVE_SIZING_FIELD_H 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 54c99b2de432..4ba686e9e191 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -218,6 +219,23 @@ void isotropic_remeshing(const FaceRange& faces np); } +template +void isotropic_remeshing(const FaceRange& faces + , const std::pair& edge_len_min_max //todo add defaults? + , const double& tolerance //todo add defaults? + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) +{ + typedef Adaptive_sizing_field Adaptive_sizing; + isotropic_remeshing( + faces, + Adaptive_sizing(edge_len_min_max, tolerance, pmesh), + pmesh, + np); +} + template Date: Fri, 19 May 2023 22:42:32 +0200 Subject: [PATCH 008/124] Create a vertex property map that will contain sizing info (WIP) Also, update target length checks --- ...sotropic_remeshing_with_sizing_example.cpp | 5 +- .../Adaptive_sizing_field.h | 57 +++++++++++++++---- .../Uniform_sizing_field.h | 2 + .../Isotropic_remeshing/remesh_impl.h | 7 ++- .../CGAL/Polygon_mesh_processing/remesh.h | 8 +-- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 96325e6d8619..1e1335ccfa6d 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -20,8 +20,8 @@ int main(int argc, char* argv[]) return 1; } - const std::pair edge_min_max{0.1, 0.4}; - const double tol = 0.1; + //todo ip - update + const std::pair edge_min_max{0.1, 0.12}; unsigned int nb_iter = 3; std::cout << "Start remeshing of " << filename @@ -30,7 +30,6 @@ int main(int argc, char* argv[]) PMP::isotropic_remeshing( faces(mesh), edge_min_max, - tol, mesh, PMP::parameters::number_of_iterations(nb_iter) ); diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 6b48181388e4..1293577ad4f8 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -34,17 +34,22 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename Base::Point_3 Point_3; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; + typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; + typedef typename boost::property_map::type VertexSizingMap; - Adaptive_sizing_field(const std::pair& edge_len_min_max - , const FT& tolerance - , const PolygonMesh& pmesh) + Adaptive_sizing_field(const std::pair& edge_len_min_max + , PolygonMesh& pmesh) : m_sq_short( CGAL::square(edge_len_min_max.first)) , m_sq_long( CGAL::square(edge_len_min_max.second)) - , tol(tolerance) , m_pmesh(pmesh) { - // calculate and store curvature and sizing field here in constructor? - // todo what about updating it? + //todo ip: initialize sizing map with default values + //todo ip: might end up using directly the property map of the curvature calculation (if mutable)? + vertex_sizing_map_ = get(Vertex_property_tag(), m_pmesh); + for(vertex_descriptor v : vertices(m_pmesh)){ + put(vertex_sizing_map_, v, m_sq_long); + } } private: @@ -62,10 +67,24 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } public: + void calc_sizing_map() + { + //todo ip + // calculate curvature + + // loop over curvature property field and calculate the target mesh size for a vertex + // don't forget to store squared length + + } + boost::optional is_too_long(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - if(sqlen > m_sq_long) + FT sqtarg_len = std::min(get(vertex_sizing_map_, source(h, m_pmesh)), + get(vertex_sizing_map_, target(h, m_pmesh))); + CGAL_assertion(get(vertex_sizing_map_, source(h, m_pmesh))); + CGAL_assertion(get(vertex_sizing_map_, target(h, m_pmesh))); + if(sqlen > sqtarg_len) return sqlen; else return boost::none; @@ -75,7 +94,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field const vertex_descriptor& vb) const { const FT sqlen = sqlength(va, vb); - if (sqlen > m_sq_long) + FT sqtarg_len = std::min(get(vertex_sizing_map_, va), + get(vertex_sizing_map_, vb)); + CGAL_assertion(get(vertex_sizing_map_, va)); + CGAL_assertion(get(vertex_sizing_map_, vb)); + if (sqlen > sqtarg_len) return sqlen; else return boost::none; @@ -84,7 +107,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_short(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - if (sqlen < m_sq_long) + FT sqtarg_len = std::min(get(vertex_sizing_map_, source(h, m_pmesh)), + get(vertex_sizing_map_, target(h, m_pmesh))); + CGAL_assertion(get(vertex_sizing_map_, source(h, m_pmesh))); + CGAL_assertion(get(vertex_sizing_map_, target(h, m_pmesh))); + if (sqlen < sqtarg_len) return sqlen; else return boost::none; @@ -98,12 +125,18 @@ class Adaptive_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } + void update_sizing_map(const vertex_descriptor& vnew) + { + //todo ip: calculate curvature for the vertex + //dummy + put(vertex_sizing_map_, vnew, m_sq_short); + } + private: FT m_sq_short; FT m_sq_long; - FT tol; - const PolygonMesh& m_pmesh; - //todo add property map containing sizing field form m_pmesh here + PolygonMesh& m_pmesh; + VertexSizingMap vertex_sizing_map_; }; }//end namespace Polygon_mesh_processing diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index 3f32aacb1aa9..fbb522b7c787 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -92,6 +92,8 @@ class Uniform_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } + void update_sizing_map(const vertex_descriptor& vnew) const {} //todo ip- rewrite to remove this? + private: FT m_sq_short; FT m_sq_long; 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 f0daf9cc20a5..19651ded9ae5 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 @@ -478,7 +478,7 @@ namespace internal { //if an edge is longer than the given threshold `high`, the edge //is split at its midpoint and the two adjacent triangles are bisected (2-4 split)" template - void split_long_edges(const SizingFunction& sizing) + void split_long_edges(SizingFunction& sizing) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << "Split long edges..." << std::endl; @@ -536,6 +536,8 @@ namespace internal { //move refinement point vertex_descriptor vnew = target(hnew, mesh_); put(vpmap_, vnew, refinement_point); + //todo ip-add + sizing.update_sizing_map(vnew); #ifdef CGAL_PMP_REMESHING_VERY_VERBOSE std::cout << " Refinement point : " << refinement_point << std::endl; #endif @@ -1080,6 +1082,7 @@ namespace internal { Point proj = trees[patch_id_to_index_map[get_patch_id(face(halfedge(v, mesh_), mesh_))]]->closest_point(get(vpmap_, v)); put(vpmap_, v, proj); + //todo ip - also update sizing field here? } CGAL_assertion(!input_mesh_is_valid_ || is_valid_polygon_mesh(mesh_)); #ifdef CGAL_PMP_REMESHING_DEBUG @@ -1108,6 +1111,7 @@ namespace internal { continue; //note if v is constrained, it has not moved put(vpmap_, v, proj(v)); + //todo ip: also update sizing field here? } CGAL_assertion(is_valid(mesh_)); #ifdef CGAL_PMP_REMESHING_DEBUG @@ -2010,6 +2014,7 @@ namespace internal { VertexIsConstrainedMap vcmap_; FaceIndexMap fimap_; CGAL_assertion_code(bool input_mesh_is_valid_;) + //todo ip: maybe make sizing field member (reference) here? easier to handle updates };//end class Incremental_remesher }//end namespace internal 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 4ba686e9e191..858f34958109 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -38,7 +38,7 @@ template void isotropic_remeshing(const FaceRange& faces - , const SizingFunction& sizing + , SizingFunction& sizing , PolygonMesh& pmesh , const NamedParameters& np); @@ -224,14 +224,14 @@ template void isotropic_remeshing(const FaceRange& faces , const std::pair& edge_len_min_max //todo add defaults? - , const double& tolerance //todo add defaults? , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { typedef Adaptive_sizing_field Adaptive_sizing; + Adaptive_sizing sizing(edge_len_min_max, pmesh); isotropic_remeshing( faces, - Adaptive_sizing(edge_len_min_max, tolerance, pmesh), + sizing, pmesh, np); } @@ -241,7 +241,7 @@ template void isotropic_remeshing(const FaceRange& faces - , const SizingFunction& sizing + , SizingFunction& sizing , PolygonMesh& pmesh , const NamedParameters& np) { From 92a434018ac1087aab8c02c1835a2a87a68e29e2 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 23 May 2023 00:09:18 +0200 Subject: [PATCH 009/124] Add sizing field calculation from curvature WIP: isotropic remeshing default overload is now broken --- .../Polygon_mesh_processing/CMakeLists.txt | 11 +- ...sotropic_remeshing_with_sizing_example.cpp | 15 +- .../Adaptive_sizing_field.h | 141 +++++++++++++----- .../Isotropic_remeshing/Sizing_field.h | 2 +- .../Uniform_sizing_field.h | 5 +- .../Isotropic_remeshing/remesh_impl.h | 9 +- .../CGAL/Polygon_mesh_processing/remesh.h | 10 +- 7 files changed, 140 insertions(+), 53 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index 28d0550a31c4..3644cc490978 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -25,9 +25,8 @@ 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("isotropic_remeshing_example.cpp") -create_single_source_cgal_program("isotropic_remeshing_of_patch_example.cpp") -create_single_source_cgal_program("isotropic_remeshing_with_sizing_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("tangential_relaxation_example.cpp") create_single_source_cgal_program("surface_mesh_intersection.cpp") create_single_source_cgal_program("corefinement_SM.cpp") @@ -70,6 +69,12 @@ if(TARGET CGAL::Eigen3_support) target_link_libraries(hole_filling_example_LCC PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("mesh_smoothing_example.cpp") target_link_libraries(mesh_smoothing_example PUBLIC CGAL::Eigen3_support) + create_single_source_cgal_program("isotropic_remeshing_example.cpp") + target_link_libraries(isotropic_remeshing_example PUBLIC CGAL::Eigen3_support) + create_single_source_cgal_program("isotropic_remeshing_of_patch_example.cpp") + target_link_libraries(isotropic_remeshing_of_patch_example PUBLIC CGAL::Eigen3_support) + create_single_source_cgal_program("isotropic_remeshing_with_sizing_example.cpp") + target_link_libraries(isotropic_remeshing_with_sizing_example PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("delaunay_remeshing_example.cpp") target_link_libraries(delaunay_remeshing_example PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("remesh_almost_planar_patches.cpp") diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 1e1335ccfa6d..fd27430e056d 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -11,7 +11,9 @@ namespace PMP = CGAL::Polygon_mesh_processing; int main(int argc, char* argv[]) { - const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/pig.off"); +// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/pig.off"); +// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/hand.off"); + const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/nefertiti.off"); std::ifstream input(filename); Mesh mesh; @@ -20,16 +22,17 @@ int main(int argc, char* argv[]) return 1; } - //todo ip - update - const std::pair edge_min_max{0.1, 0.12}; - unsigned int nb_iter = 3; - std::cout << "Start remeshing of " << filename << " (" << num_faces(mesh) << " faces)..." << std::endl; + const double tol = 0.002; + const std::pair edge_min_max{0.001, 0.5}; + PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, mesh); + unsigned int nb_iter = 3; + PMP::isotropic_remeshing( faces(mesh), - edge_min_max, + sizing_field, mesh, PMP::parameters::number_of_iterations(nb_iter) ); diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 1293577ad4f8..5724d26ba0df 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -17,6 +17,8 @@ #include +#include + #include namespace CGAL @@ -30,26 +32,37 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef CGAL::Sizing_field Base; public: + typedef typename Base::K K; typedef typename Base::FT FT; typedef typename Base::Point_3 Point_3; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; - typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; + + typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map::type VertexSizingMap; - Adaptive_sizing_field(const std::pair& edge_len_min_max + //todo ip: set a property map that can calculate curvature in one go. I think I'm generating constant maps (without put) + // try 1 + typedef Principal_curvatures_and_directions Principal_curvatures; +// typedef Constant_property_map Vertex_curvature_map; + + // try 2 + typedef Constant_property_map> Default_principal_map; + typedef typename internal_np::Lookup_named_param_def::type + Vertex_curvature_map; + + Adaptive_sizing_field(const double tol + , const std::pair& edge_len_min_max , PolygonMesh& pmesh) - : m_sq_short( CGAL::square(edge_len_min_max.first)) - , m_sq_long( CGAL::square(edge_len_min_max.second)) + : tol(tol) + , m_sq_short(CGAL::square(edge_len_min_max.first)) + , m_sq_long( CGAL::square(edge_len_min_max.second)) , m_pmesh(pmesh) { - //todo ip: initialize sizing map with default values - //todo ip: might end up using directly the property map of the curvature calculation (if mutable)? - vertex_sizing_map_ = get(Vertex_property_tag(), m_pmesh); - for(vertex_descriptor v : vertices(m_pmesh)){ - put(vertex_sizing_map_, v, m_sq_long); - } + m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); } private: @@ -69,21 +82,62 @@ class Adaptive_sizing_field : public CGAL::Sizing_field public: void calc_sizing_map() { - //todo ip - // calculate curvature - - // loop over curvature property field and calculate the target mesh size for a vertex - // don't forget to store squared length - +#ifdef CGAL_PMP_REMESHING_VERBOSE + int oversize = 0; + int undersize = 0; + int insize = 0; + std::cout << "Calculating sizing field..." << std::endl; +#endif + + //todo ip: how to make this work? +// Vertex_curvature_map vertex_curvature_map; +// interpolated_corrected_principal_curvatures_and_directions(m_pmesh +// , vertex_curvature_map); + + // calculate square vertex sizing field (L(x_i))^2 from curvature field + for(vertex_descriptor v : vertices(m_pmesh)) + { +// auto vertex_curv = get(vertex_curvature_map, v); //todo ip: how to make this work? + //todo ip: temp solution + const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); + const FT max_absolute_curv = std::max(std::abs(vertex_curv.max_curvature), std::abs(vertex_curv.min_curvature)); + const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); + if (vertex_size_sq > m_sq_long) + { + put(m_vertex_sizing_map, v, m_sq_long); +#ifdef CGAL_PMP_REMESHING_VERBOSE + ++oversize; +#endif + } + else if (vertex_size_sq < m_sq_short) + { + put(m_vertex_sizing_map, v, m_sq_short); +#ifdef CGAL_PMP_REMESHING_VERBOSE + ++undersize; +#endif + } + else + { + put(m_vertex_sizing_map, v, vertex_size_sq); +#ifdef CGAL_PMP_REMESHING_VERBOSE + ++insize; +#endif + } + } +#ifdef CGAL_PMP_REMESHING_VERBOSE + std::cout << " done (" << insize << " from curvature, " + << oversize << " set to max, " + << undersize << " set to min)" << std::endl; +#endif } boost::optional is_too_long(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = std::min(get(vertex_sizing_map_, source(h, m_pmesh)), - get(vertex_sizing_map_, target(h, m_pmesh))); - CGAL_assertion(get(vertex_sizing_map_, source(h, m_pmesh))); - CGAL_assertion(get(vertex_sizing_map_, target(h, m_pmesh))); + FT sqtarg_len = std::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); if(sqlen > sqtarg_len) return sqlen; else @@ -94,11 +148,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field const vertex_descriptor& vb) const { const FT sqlen = sqlength(va, vb); - FT sqtarg_len = std::min(get(vertex_sizing_map_, va), - get(vertex_sizing_map_, vb)); - CGAL_assertion(get(vertex_sizing_map_, va)); - CGAL_assertion(get(vertex_sizing_map_, vb)); - if (sqlen > sqtarg_len) + FT sqtarg_len = std::min(get(m_vertex_sizing_map, va), + get(m_vertex_sizing_map, vb)); + CGAL_assertion(get(m_vertex_sizing_map, va)); + CGAL_assertion(get(m_vertex_sizing_map, vb)); + if (sqlen > 16./9. * sqtarg_len) return sqlen; else return boost::none; @@ -107,11 +161,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_short(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = std::min(get(vertex_sizing_map_, source(h, m_pmesh)), - get(vertex_sizing_map_, target(h, m_pmesh))); - CGAL_assertion(get(vertex_sizing_map_, source(h, m_pmesh))); - CGAL_assertion(get(vertex_sizing_map_, target(h, m_pmesh))); - if (sqlen < sqtarg_len) + FT sqtarg_len = std::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); + if (sqlen < 16./25. * sqtarg_len) return sqlen; else return boost::none; @@ -125,18 +179,31 @@ class Adaptive_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } - void update_sizing_map(const vertex_descriptor& vnew) + void update_sizing_map(const vertex_descriptor& v) { - //todo ip: calculate curvature for the vertex - //dummy - put(vertex_sizing_map_, vnew, m_sq_short); + // calculating it as the average of two vertices on other ends + // of halfedges as updating is done during an edge split + int i = 0; + FT vertex_size_sq = 0; + CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh) == 2); + for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, m_pmesh)) + { + vertex_size_sq += get(m_vertex_sizing_map, source(ha, m_pmesh)); + ++i; + } + vertex_size_sq /= i; + + put(m_vertex_sizing_map, v, vertex_size_sq); } + //todo ip: is_protected_constraint_too_long() from PR + private: - FT m_sq_short; - FT m_sq_long; + const FT tol; + const FT m_sq_short; + const FT m_sq_long; PolygonMesh& m_pmesh; - VertexSizingMap vertex_sizing_map_; + VertexSizingMap m_vertex_sizing_map; }; }//end namespace Polygon_mesh_processing diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index fc6b14a984f5..7f1a8a2abbea 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -30,9 +30,9 @@ class Sizing_field typedef PolygonMesh PM; typedef typename boost::property_map::const_type VPMap; typedef typename boost::property_traits::value_type Point; - typedef typename CGAL::Kernel_traits::Kernel K; public: + typedef typename CGAL::Kernel_traits::Kernel K; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef Point Point_3; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index fbb522b7c787..ce523b30a8a9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -56,6 +56,10 @@ class Uniform_sizing_field : public CGAL::Sizing_field } public: + //todo ip: rewrite to remove this? + void calc_sizing_map() const {} + void update_sizing_map(const vertex_descriptor& vnew) const {} + boost::optional is_too_long(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); @@ -92,7 +96,6 @@ class Uniform_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } - void update_sizing_map(const vertex_descriptor& vnew) const {} //todo ip- rewrite to remove this? private: FT m_sq_short; 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 19651ded9ae5..20647a91cb77 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 @@ -55,6 +55,9 @@ #include #include +//todo ip: temp +#define CGAL_PMP_REMESHING_VERBOSE + #ifdef CGAL_PMP_REMESHING_DEBUG #include #define CGAL_DUMP_REMESHING_STEPS @@ -536,8 +539,6 @@ namespace internal { //move refinement point vertex_descriptor vnew = target(hnew, mesh_); put(vpmap_, vnew, refinement_point); - //todo ip-add - sizing.update_sizing_map(vnew); #ifdef CGAL_PMP_REMESHING_VERY_VERBOSE std::cout << " Refinement point : " << refinement_point << std::endl; #endif @@ -547,6 +548,9 @@ namespace internal { halfedge_added(hnew, status(he)); halfedge_added(hnew_opp, status(opposite(he, mesh_))); + //todo ip-add: already updating sizing here because of is_too_long checks below + sizing.update_sizing_map(vnew); + //check sub-edges //if it was more than twice the "long" threshold, insert them boost::optional sqlen_new = sizing.is_too_long(hnew); @@ -2014,7 +2018,6 @@ namespace internal { VertexIsConstrainedMap vcmap_; FaceIndexMap fimap_; CGAL_assertion_code(bool input_mesh_is_valid_;) - //todo ip: maybe make sizing field member (reference) here? easier to handle updates };//end class Incremental_remesher }//end namespace internal 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 858f34958109..33f382ac4639 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -212,18 +212,22 @@ void isotropic_remeshing(const FaceRange& faces , const NamedParameters& np = parameters::default_values()) { typedef Uniform_sizing_field Default_sizing; + Default_sizing sizing(target_edge_length, pmesh); isotropic_remeshing( faces, - Default_sizing(target_edge_length, pmesh), + sizing, pmesh, np); } +//todo ip: should I have the overload here? +/* template void isotropic_remeshing(const FaceRange& faces - , const std::pair& edge_len_min_max //todo add defaults? + , const double& tol + , const std::pair& edge_len_min_max , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { @@ -235,6 +239,7 @@ void isotropic_remeshing(const FaceRange& faces pmesh, np); } + */ template Date: Wed, 24 May 2023 20:10:00 +0200 Subject: [PATCH 010/124] Refactor sizing map update --- .../internal/Isotropic_remeshing/Adaptive_sizing_field.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 5724d26ba0df..64ac1ddf5613 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -183,15 +183,13 @@ class Adaptive_sizing_field : public CGAL::Sizing_field { // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split - int i = 0; FT vertex_size_sq = 0; CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh) == 2); for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, m_pmesh)) { vertex_size_sq += get(m_vertex_sizing_map, source(ha, m_pmesh)); - ++i; } - vertex_size_sq /= i; + vertex_size_sq /= CGAL::halfedges_around_target(v, m_pmesh).size(); put(m_vertex_sizing_map, v, vertex_size_sq); } From 52df5ae86eeddea3ae0a08fa79bce3afb668c64c Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 26 May 2023 11:03:27 +0200 Subject: [PATCH 011/124] Fix default remeshing overload --- .../include/CGAL/Polygon_mesh_processing/remesh.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 33f382ac4639..2c31cf4c65f6 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -207,7 +207,7 @@ template void isotropic_remeshing(const FaceRange& faces - , const double& target_edge_length + , const double target_edge_length , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { @@ -355,13 +355,15 @@ void isotropic_remeshing(const FaceRange& faces t.reset(); t.start(); #endif +// sizing.calc_sizing_map(); for (unsigned int i = 0; i < nb_iterations; ++i) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " * Iteration " << (i + 1) << " *" << std::endl; #endif - sizing.calc_sizing_map(); + if (i < 2) + sizing.calc_sizing_map(); if(do_split) remesher.split_long_edges(sizing); if(do_collapse) From 947ab8f1255d7bedea2adb6be919f23f95f3fd7b Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 26 May 2023 18:20:11 +0200 Subject: [PATCH 012/124] Make a (temp) property map for curvature calculation --- .../isotropic_remeshing_with_sizing_example.cpp | 7 ++++--- .../Isotropic_remeshing/Adaptive_sizing_field.h | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index fd27430e056d..cfbf7bfd9c77 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -11,9 +11,10 @@ namespace PMP = CGAL::Polygon_mesh_processing; int main(int argc, char* argv[]) { -// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/pig.off"); +// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/lion-head.off"); // const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/hand.off"); const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/nefertiti.off"); +// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube.off"); std::ifstream input(filename); Mesh mesh; @@ -25,10 +26,10 @@ int main(int argc, char* argv[]) std::cout << "Start remeshing of " << filename << " (" << num_faces(mesh) << " faces)..." << std::endl; - const double tol = 0.002; + const double tol = 0.001; const std::pair edge_min_max{0.001, 0.5}; PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, mesh); - unsigned int nb_iter = 3; + unsigned int nb_iter = 5; PMP::isotropic_remeshing( faces(mesh), diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 64ac1ddf5613..1cbaf0bce8df 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -91,15 +91,19 @@ class Adaptive_sizing_field : public CGAL::Sizing_field //todo ip: how to make this work? // Vertex_curvature_map vertex_curvature_map; -// interpolated_corrected_principal_curvatures_and_directions(m_pmesh -// , vertex_curvature_map); + + //todo ip: temp workaround + auto vertex_curvature_map = + m_pmesh.template add_property_map>("v:curvature_map").first; + interpolated_corrected_principal_curvatures_and_directions(m_pmesh + , vertex_curvature_map); // calculate square vertex sizing field (L(x_i))^2 from curvature field for(vertex_descriptor v : vertices(m_pmesh)) { -// auto vertex_curv = get(vertex_curvature_map, v); //todo ip: how to make this work? - //todo ip: temp solution - const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); + auto vertex_curv = get(vertex_curvature_map, v); //todo ip: how to make this work? + //todo ip: alt solution - calculate curvature per vertex +// const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); const FT max_absolute_curv = std::max(std::abs(vertex_curv.max_curvature), std::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); if (vertex_size_sq > m_sq_long) From c89bedb97faac6a194086ced0d3cdb063403b913 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 31 May 2023 22:26:23 +0200 Subject: [PATCH 013/124] Replace std with cgal where applicable, fix assertion --- .../Adaptive_sizing_field.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 1cbaf0bce8df..9027fa0e40f3 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -101,10 +101,10 @@ class Adaptive_sizing_field : public CGAL::Sizing_field // calculate square vertex sizing field (L(x_i))^2 from curvature field for(vertex_descriptor v : vertices(m_pmesh)) { - auto vertex_curv = get(vertex_curvature_map, v); //todo ip: how to make this work? + auto vertex_curv = get(vertex_curvature_map, v); //todo ip: alt solution - calculate curvature per vertex // const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); - const FT max_absolute_curv = std::max(std::abs(vertex_curv.max_curvature), std::abs(vertex_curv.min_curvature)); + const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature), CGAL::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); if (vertex_size_sq > m_sq_long) { @@ -138,8 +138,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_long(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = std::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh))); + FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); if(sqlen > sqtarg_len) @@ -152,8 +152,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field const vertex_descriptor& vb) const { const FT sqlen = sqlength(va, vb); - FT sqtarg_len = std::min(get(m_vertex_sizing_map, va), - get(m_vertex_sizing_map, vb)); + FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, va), + get(m_vertex_sizing_map, vb)); CGAL_assertion(get(m_vertex_sizing_map, va)); CGAL_assertion(get(m_vertex_sizing_map, vb)); if (sqlen > 16./9. * sqtarg_len) @@ -165,8 +165,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_short(const halfedge_descriptor& h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = std::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh))); + FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); if (sqlen < 16./25. * sqtarg_len) @@ -188,7 +188,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split FT vertex_size_sq = 0; - CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh) == 2); + CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh).size() == 2); for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, m_pmesh)) { vertex_size_sq += get(m_vertex_sizing_map, source(ha, m_pmesh)); From fa9769b908026cd63c4801ba34fb73b8d2f055c7 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 8 Jun 2023 23:08:40 +0200 Subject: [PATCH 014/124] Prep sizing for tangential relaxation (WIP) --- .../Isotropic_remeshing/remesh_impl.h | 18 +- .../CGAL/Polygon_mesh_processing/remesh.h | 8 +- .../tangential_relaxation.h | 210 ++++++++++++++++++ 3 files changed, 231 insertions(+), 5 deletions(-) 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 20647a91cb77..6b2fa17bb0b9 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 @@ -1012,8 +1012,10 @@ namespace internal { // "applies an iterative smoothing filter to the mesh. // The vertex movement has to be constrained to the vertex tangent plane [...] // smoothing algorithm with uniform Laplacian weights" + template void tangential_relaxation_impl(const bool relax_constraints/*1d smoothing*/ - , const unsigned int nb_iterations) + , const unsigned int nb_iterations + , const SizingFunction& sizing) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << "Tangential relaxation (" << nb_iterations << " iter.)..."; @@ -1044,6 +1046,8 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); + //todo IP temp: I have to rewrite to include original implementation + /* tangential_relaxation( vertices(mesh_), mesh_, @@ -1054,6 +1058,18 @@ namespace internal { .vertex_is_constrained_map(constrained_vertices_pmap) .relax_constraints(relax_constraints) ); + */ + tangential_relaxation( + vertices(mesh_), + mesh_, + sizing, + CGAL::parameters::number_of_iterations(nb_iterations) + .vertex_point_map(vpmap_) + .geom_traits(gt_) + .edge_is_constrained_map(constrained_edges_pmap) + .vertex_is_constrained_map(constrained_vertices_pmap) + .relax_constraints(relax_constraints) + ); CGAL_assertion(!input_mesh_is_valid_ || is_valid_polygon_mesh(mesh_)); 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 2c31cf4c65f6..4f5162482c29 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -355,22 +355,22 @@ void isotropic_remeshing(const FaceRange& faces t.reset(); t.start(); #endif -// sizing.calc_sizing_map(); + sizing.calc_sizing_map(); for (unsigned int i = 0; i < nb_iterations; ++i) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " * Iteration " << (i + 1) << " *" << std::endl; #endif - if (i < 2) - sizing.calc_sizing_map(); +// if (i < 2) +// sizing.calc_sizing_map(); if(do_split) remesher.split_long_edges(sizing); if(do_collapse) remesher.collapse_short_edges(sizing, collapse_constraints); if(do_flip) remesher.flip_edges_for_valence_and_shape(); - remesher.tangential_relaxation_impl(smoothing_1d, nb_laplacian); + remesher.tangential_relaxation_impl(smoothing_1d, nb_laplacian, sizing); if ( choose_parameter(get_parameter(np, internal_np::do_project), true) ) remesher.project_to_surface(get_parameter(np, internal_np::projection_functor)); #ifdef CGAL_PMP_REMESHING_VERBOSE diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index d14e513d347a..19ebdda8650e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -317,6 +317,208 @@ void tangential_relaxation(const VertexRange& vertices, #endif } +template +void tangential_relaxation(const VertexRange& vertices, + TriangleMesh& tm, + const SizingFunction& sizing, + const NamedParameters& np = parameters::default_values()) +{ + typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; + typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; + typedef typename boost::graph_traits::edge_descriptor edge_descriptor; + + using parameters::get_parameter; + using parameters::choose_parameter; + + typedef typename GetGeomTraits::type GT; + GT gt = choose_parameter(get_parameter(np, internal_np::geom_traits), GT()); + + typedef typename GetVertexPointMap::type VPMap; + VPMap vpm = choose_parameter(get_parameter(np, internal_np::vertex_point), + get_property_map(vertex_point, tm)); + + typedef Static_boolean_property_map Default_ECM; + typedef typename internal_np::Lookup_named_param_def < + internal_np::edge_is_constrained_t, + NamedParameters, + Static_boolean_property_map // default (no constraint) + > ::type ECM; + ECM ecm = choose_parameter(get_parameter(np, internal_np::edge_is_constrained), + Default_ECM()); + + typedef typename internal_np::Lookup_named_param_def < + internal_np::vertex_is_constrained_t, + NamedParameters, + Static_boolean_property_map // default (no constraint) + > ::type VCM; + VCM vcm = choose_parameter(get_parameter(np, internal_np::vertex_is_constrained), + Static_boolean_property_map()); + + const bool relax_constraints = choose_parameter(get_parameter(np, internal_np::relax_constraints), false); + const unsigned int nb_iterations = choose_parameter(get_parameter(np, internal_np::number_of_iterations), 1); + + typedef typename GT::Vector_3 Vector_3; + typedef typename GT::Point_3 Point_3; + + auto check_normals = [&](vertex_descriptor v) + { + bool first_run = true; + Vector_3 prev = NULL_VECTOR, first = NULL_VECTOR; + halfedge_descriptor first_h = boost::graph_traits::null_halfedge(); + for (halfedge_descriptor hd : CGAL::halfedges_around_target(v, tm)) + { + if (is_border(hd, tm)) continue; + + Vector_3 n = compute_face_normal(face(hd, tm), tm, np); + if (n == CGAL::NULL_VECTOR) //for degenerate faces + continue; + + if (first_run) + { + first_run = false; + first = n; + first_h = hd; + } + else + { + if (!get(ecm, edge(hd, tm))) + if (to_double(n * prev) <= 0) + return false; + } + prev = n; + } + + if (first_run) + return true; //vertex incident only to degenerate faces + + if (!get(ecm, edge(first_h, tm))) + if (to_double(first * prev) <= 0) + return false; + + return true; + }; + + typedef typename internal_np::Lookup_named_param_def < + internal_np::allow_move_functor_t, + NamedParameters, + internal::Allow_all_moves// default + > ::type Shall_move; + Shall_move shall_move = choose_parameter(get_parameter(np, internal_np::allow_move_functor), + internal::Allow_all_moves()); + + for (unsigned int nit = 0; nit < nb_iterations; ++nit) + { +#ifdef CGAL_PMP_TANGENTIAL_RELAXATION_VERBOSE + std::cout << "\r\t(Tangential relaxation iteration " << (nit + 1) << " / "; + std::cout << nb_iterations << ") "; + std::cout.flush(); +#endif + + typedef std::tuple VNP; + std::vector< VNP > barycenters; + auto gt_barycenter = gt.construct_barycenter_3_object(); + + // at each vertex, compute vertex normal + std::unordered_map vnormals; + compute_vertex_normals(tm, boost::make_assoc_property_map(vnormals), np); + + // at each vertex, compute barycenter of neighbors + for(vertex_descriptor v : vertices) + { + if (get(vcm, v) || CGAL::internal::is_isolated(v, tm)) + continue; + + // collect hedges to detect if we have to handle boundary cases + std::vector interior_hedges, border_halfedges; + for(halfedge_descriptor h : halfedges_around_target(v, tm)) + { + if (is_border_edge(h, tm) || get(ecm, edge(h, tm))) + border_halfedges.push_back(h); + else + interior_hedges.push_back(h); + } + + if (border_halfedges.empty()) + { + const Vector_3& vn = vnormals.at(v); + Vector_3 move = CGAL::NULL_VECTOR; + unsigned int star_size = 0; + for(halfedge_descriptor h :interior_hedges) + { + move = move + Vector_3(get(vpm, v), get(vpm, source(h, tm))); + ++star_size; + } + CGAL_assertion(star_size > 0); //isolated vertices have already been discarded + move = (1. / static_cast(star_size)) * move; + + barycenters.emplace_back(v, vn, get(vpm, v) + move); + } + else + { + if (!relax_constraints) continue; + Vector_3 vn(NULL_VECTOR); + + if (border_halfedges.size() == 2)// corners are constrained + { + vertex_descriptor ph0 = source(border_halfedges[0], tm); + vertex_descriptor ph1 = source(border_halfedges[1], tm); + double dot = to_double(Vector_3(get(vpm, v), get(vpm, ph0)) + * Vector_3(get(vpm, v), get(vpm, ph1))); + // \todo shouldn't it be an input parameter? + //check squared cosine is < 0.25 (~120 degrees) + if (0.25 < dot*dot / ( squared_distance(get(vpm,ph0), get(vpm, v)) * + squared_distance(get(vpm,ph1), get(vpm, v))) ) + barycenters.emplace_back(v, vn, + gt_barycenter(get(vpm, ph0), 0.25, get(vpm, ph1), 0.25, get(vpm, v), 0.5)); + } + } + } + + // compute moves + typedef std::pair VP_pair; + std::vector< std::pair > new_locations; + new_locations.reserve(barycenters.size()); + for(const VNP& vnp : barycenters) + { + vertex_descriptor v = std::get<0>(vnp); + const Point_3& pv = get(vpm, v); + const Vector_3& nv = std::get<1>(vnp); + const Point_3& qv = std::get<2>(vnp); //barycenter at v + + new_locations.emplace_back(v, qv + (nv * Vector_3(qv, pv)) * nv); + } + + // perform moves + for(const VP_pair& vp : new_locations) + { + const Point_3 initial_pos = get(vpm, vp.first); // make a copy on purpose + const Vector_3 move(initial_pos, vp.second); + + put(vpm, vp.first, vp.second); + + //check that no inversion happened + double frac = 1.; + while (frac > 0.03 //5 attempts maximum + && ( !check_normals(vp.first) + || !shall_move(vp.first, initial_pos, get(vpm, vp.first)))) //if a face has been inverted + { + frac = 0.5 * frac; + put(vpm, vp.first, initial_pos + frac * move);//shorten the move by 2 + } + if (frac <= 0.02) + put(vpm, vp.first, initial_pos);//cancel move + } + }//end for loop (nit == nb_iterations) + +#ifdef CGAL_PMP_TANGENTIAL_RELAXATION_VERBOSE + std::cout << "\rTangential relaxation : " + << nb_iterations << " iterations done." << std::endl; +#endif +} + /*! * \ingroup PMP_meshing_grp * applies `tangential_relaxation()` to all the vertices of `tm`. @@ -328,6 +530,14 @@ void tangential_relaxation(TriangleMesh& tm, const CGAL_NP_CLASS& np = parameter tangential_relaxation(vertices(tm), tm, np); } +template +void tangential_relaxation(TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) +{ + tangential_relaxation(vertices(tm), tm, sizing, np); +} + } } // CGAL::Polygon_mesh_processing #endif //CGAL_POLYGON_MESH_PROCESSING_TANGENTIAL_RELAXATION_H From 5629f7a04b642c2ca2ddfcc80aa1540088ab5232 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 13 Jun 2023 00:14:23 +0200 Subject: [PATCH 015/124] Add first code for tangential relaxation with sizing (WIP) --- .../Adaptive_sizing_field.h | 5 ++ .../Isotropic_remeshing/remesh_impl.h | 10 ++-- .../CGAL/Polygon_mesh_processing/remesh.h | 1 + .../tangential_relaxation.h | 46 +++++++++++++++---- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 9027fa0e40f3..d49ca57c8dce 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -80,6 +80,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } public: + FT get_sizing(const vertex_descriptor& v) const { + CGAL_assertion(get(m_vertex_sizing_map, v)); + return get(m_vertex_sizing_map, v); + } + void calc_sizing_map() { #ifdef CGAL_PMP_REMESHING_VERBOSE 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 6b2fa17bb0b9..581b9870778c 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 @@ -1046,8 +1046,9 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); - //todo IP temp: I have to rewrite to include original implementation - /* + //todo IP temp: I have to rewrite to include original implementation, hardcoded for now + const bool use_sizing = true; + if (!use_sizing) tangential_relaxation( vertices(mesh_), mesh_, @@ -1058,8 +1059,9 @@ namespace internal { .vertex_is_constrained_map(constrained_vertices_pmap) .relax_constraints(relax_constraints) ); - */ - tangential_relaxation( + + else + tangential_relaxation_with_sizing( vertices(mesh_), mesh_, sizing, 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 4f5162482c29..975ff7bbf9b0 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -371,6 +371,7 @@ void isotropic_remeshing(const FaceRange& faces if(do_flip) remesher.flip_edges_for_valence_and_shape(); remesher.tangential_relaxation_impl(smoothing_1d, nb_laplacian, sizing); +// remesher.tangential_relaxation_impl(smoothing_1d, nb_laplacian); if ( choose_parameter(get_parameter(np, internal_np::do_project), true) ) remesher.project_to_surface(get_parameter(np, internal_np::projection_functor)); #ifdef CGAL_PMP_REMESHING_VERBOSE diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 19ebdda8650e..d4f5dd8157a5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -321,7 +321,7 @@ template -void tangential_relaxation(const VertexRange& vertices, +void tangential_relaxation_with_sizing(const VertexRange& vertices, TriangleMesh& tm, const SizingFunction& sizing, const NamedParameters& np = parameters::default_values()) @@ -363,6 +363,12 @@ void tangential_relaxation(const VertexRange& vertices, typedef typename GT::Vector_3 Vector_3; typedef typename GT::Point_3 Point_3; + //todo ip: alt calc + typename GT::Construct_vector_3 vector = gt.construct_vector_3_object(); + typename GT::Compute_scalar_product_3 scalar_product = gt.compute_scalar_product_3_object(); + typename GT::Compute_squared_length_3 squared_length = gt.compute_squared_length_3_object(); + typename GT::Construct_cross_product_vector_3 cross_product = gt.construct_cross_product_vector_3_object(); + auto check_normals = [&](vertex_descriptor v) { bool first_run = true; @@ -420,12 +426,15 @@ void tangential_relaxation(const VertexRange& vertices, typedef std::tuple VNP; std::vector< VNP > barycenters; auto gt_barycenter = gt.construct_barycenter_3_object(); + auto gt_centroid = gt.construct_centroid_3_object(); + auto gt_area = gt.compute_area_3_object(); // at each vertex, compute vertex normal std::unordered_map vnormals; compute_vertex_normals(tm, boost::make_assoc_property_map(vnormals), np); - // at each vertex, compute barycenter of neighbors + // at each vertex, compute centroids of neighbouring faces weighted by + // area and sizing field for(vertex_descriptor v : vertices) { if (get(vcm, v) || CGAL::internal::is_isolated(v, tm)) @@ -441,18 +450,37 @@ void tangential_relaxation(const VertexRange& vertices, interior_hedges.push_back(h); } + //todo ip: handle border edges with sizing field if (border_halfedges.empty()) { const Vector_3& vn = vnormals.at(v); Vector_3 move = CGAL::NULL_VECTOR; - unsigned int star_size = 0; + double weight = 0; +// unsigned int star_size = 0; for(halfedge_descriptor h :interior_hedges) { - move = move + Vector_3(get(vpm, v), get(vpm, source(h, tm))); - ++star_size; + // calculate weight + // need v0, v1 and v2 + const vertex_descriptor v1 = target(next(h, tm), tm); + const vertex_descriptor v2 = source(h, tm); + + //todo ip- alt calc + const Vector_3 vec0 = vector(get(vpm, v), get(vpm, v1)); + const Vector_3 vec1 = vector(get(vpm, v), get(vpm, v2)); + const double sqarea = squared_length(cross_product(vec0, vec1)); + const double face_weight = CGAL::approximate_sqrt(sqarea) + / pow(1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2)), 2); + + //todo ip- paper implementation + // const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); + // const double face_weight = tri_area + // / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); + weight += face_weight; + + const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); + move = move + Vector_3(get(vpm, v), centroid) * face_weight; } - CGAL_assertion(star_size > 0); //isolated vertices have already been discarded - move = (1. / static_cast(star_size)) * move; + move = move / weight; //todo ip: what if weight ends up being close to 0? barycenters.emplace_back(v, vn, get(vpm, v) + move); } @@ -533,9 +561,9 @@ void tangential_relaxation(TriangleMesh& tm, const CGAL_NP_CLASS& np = parameter template -void tangential_relaxation(TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) +void tangential_relaxation_with_sizing(TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) { - tangential_relaxation(vertices(tm), tm, sizing, np); + tangential_relaxation_with_sizing(vertices(tm), tm, sizing, np); } } } // CGAL::Polygon_mesh_processing From cb779038f68d5622e564d8ba6dad5bb4a71e1b1d Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Tue, 13 Jun 2023 10:40:33 +0200 Subject: [PATCH 016/124] refs are not needed here Co-authored-by: Sebastien Loriot --- .../Adaptive_sizing_field.h | 18 +++++++++--------- .../Isotropic_remeshing/Sizing_field.h | 10 +++++----- .../Isotropic_remeshing/Uniform_sizing_field.h | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index d49ca57c8dce..7c86e1420499 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -66,8 +66,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } private: - FT sqlength(const vertex_descriptor& va, - const vertex_descriptor& vb) const + FT sqlength(const vertex_descriptor va, + const vertex_descriptor vb) const { typename boost::property_map::const_type vpmap = get(CGAL::vertex_point, m_pmesh); @@ -80,7 +80,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } public: - FT get_sizing(const vertex_descriptor& v) const { + FT get_sizing(const vertex_descriptor v) const { CGAL_assertion(get(m_vertex_sizing_map, v)); return get(m_vertex_sizing_map, v); } @@ -140,7 +140,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field #endif } - boost::optional is_too_long(const halfedge_descriptor& h) const + boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), @@ -153,8 +153,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_long(const vertex_descriptor& va, - const vertex_descriptor& vb) const + boost::optional is_too_long(const vertex_descriptor va, + const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, va), @@ -167,7 +167,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_short(const halfedge_descriptor& h) const + boost::optional is_too_short(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), @@ -180,7 +180,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor& h) const + virtual Point_3 split_placement(const halfedge_descriptor h) const { typename boost::property_map::const_type vpmap = get(CGAL::vertex_point, m_pmesh); @@ -188,7 +188,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } - void update_sizing_map(const vertex_descriptor& v) + void update_sizing_map(const vertex_descriptor v) { // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index 7f1a8a2abbea..359a9fc6b562 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -39,11 +39,11 @@ class Sizing_field typedef typename K::FT FT; public: - virtual boost::optional is_too_long(const halfedge_descriptor& h) const = 0; - virtual boost::optional is_too_long(const vertex_descriptor& va, - const vertex_descriptor& vb) const = 0; - virtual boost::optional is_too_short(const halfedge_descriptor& h) const = 0; - virtual Point_3 split_placement(const halfedge_descriptor& h) const = 0; + virtual boost::optional is_too_long(const halfedge_descriptor h) const = 0; + virtual boost::optional is_too_long(const vertex_descriptor va, + const vertex_descriptor vb) const = 0; + virtual boost::optional is_too_short(const halfedge_descriptor h) const = 0; + virtual Point_3 split_placement(const halfedge_descriptor h) const = 0; }; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index ce523b30a8a9..cc229583b384 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -42,8 +42,8 @@ class Uniform_sizing_field : public CGAL::Sizing_field {} private: - FT sqlength(const vertex_descriptor& va, - const vertex_descriptor& vb) const + FT sqlength(const vertex_descriptor va, + const vertex_descriptor vb) const { typename boost::property_map::const_type vpmap = get(CGAL::vertex_point, m_pmesh); @@ -60,7 +60,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field void calc_sizing_map() const {} void update_sizing_map(const vertex_descriptor& vnew) const {} - boost::optional is_too_long(const halfedge_descriptor& h) const + boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); if(sqlen > m_sq_long) @@ -69,8 +69,8 @@ class Uniform_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_long(const vertex_descriptor& va, - const vertex_descriptor& vb) const + boost::optional is_too_long(const vertex_descriptor va, + const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); if (sqlen > m_sq_long) @@ -79,7 +79,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_short(const halfedge_descriptor& h) const + boost::optional is_too_short(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); if (sqlen < m_sq_long) @@ -88,7 +88,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor& h) const + virtual Point_3 split_placement(const halfedge_descriptor h) const { typename boost::property_map::const_type vpmap = get(CGAL::vertex_point, m_pmesh); From ace36a2bb69388bfcab202e08dd0bd99f79aac8d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 20 Jun 2023 18:08:20 +0200 Subject: [PATCH 017/124] Make tangential relaxation work with both uniform and adaptive sizing field --- .../Uniform_sizing_field.h | 1 + .../Isotropic_remeshing/remesh_impl.h | 53 ++++++++++--------- .../CGAL/Polygon_mesh_processing/remesh.h | 25 +-------- .../tangential_relaxation.h | 24 +++++---- 4 files changed, 43 insertions(+), 60 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index cc229583b384..8fd4bf2334c1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -59,6 +59,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field //todo ip: rewrite to remove this? void calc_sizing_map() const {} void update_sizing_map(const vertex_descriptor& vnew) const {} + double get_sizing(vertex_descriptor v) const {return 1.;} boost::optional is_too_long(const halfedge_descriptor h) const { 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 581b9870778c..76e285176346 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 @@ -75,7 +75,11 @@ #endif namespace CGAL { + namespace Polygon_mesh_processing { + +template class Uniform_sizing_field; + namespace internal { enum Halfedge_status { @@ -1012,7 +1016,7 @@ namespace internal { // "applies an iterative smoothing filter to the mesh. // The vertex movement has to be constrained to the vertex tangent plane [...] // smoothing algorithm with uniform Laplacian weights" - template + template void tangential_relaxation_impl(const bool relax_constraints/*1d smoothing*/ , const unsigned int nb_iterations , const SizingFunction& sizing) @@ -1046,32 +1050,29 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); - //todo IP temp: I have to rewrite to include original implementation, hardcoded for now - const bool use_sizing = true; - if (!use_sizing) - tangential_relaxation( - vertices(mesh_), - mesh_, - CGAL::parameters::number_of_iterations(nb_iterations) - .vertex_point_map(vpmap_) - .geom_traits(gt_) - .edge_is_constrained_map(constrained_edges_pmap) - .vertex_is_constrained_map(constrained_vertices_pmap) - .relax_constraints(relax_constraints) - ); - + if (std::is_same>::value) + tangential_relaxation( + vertices(mesh_), + mesh_, + CGAL::parameters::number_of_iterations(nb_iterations) + .vertex_point_map(vpmap_) + .geom_traits(gt_) + .edge_is_constrained_map(constrained_edges_pmap) + .vertex_is_constrained_map(constrained_vertices_pmap) + .relax_constraints(relax_constraints) + ); else - tangential_relaxation_with_sizing( - vertices(mesh_), - mesh_, - sizing, - CGAL::parameters::number_of_iterations(nb_iterations) - .vertex_point_map(vpmap_) - .geom_traits(gt_) - .edge_is_constrained_map(constrained_edges_pmap) - .vertex_is_constrained_map(constrained_vertices_pmap) - .relax_constraints(relax_constraints) - ); + tangential_relaxation_with_sizing( + vertices(mesh_), + mesh_, + sizing, + CGAL::parameters::number_of_iterations(nb_iterations) + .vertex_point_map(vpmap_) + .geom_traits(gt_) + .edge_is_constrained_map(constrained_edges_pmap) + .vertex_is_constrained_map(constrained_vertices_pmap) + .relax_constraints(relax_constraints) + ); CGAL_assertion(!input_mesh_is_valid_ || is_valid_polygon_mesh(mesh_)); 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 975ff7bbf9b0..787fe54b9930 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -220,27 +220,6 @@ void isotropic_remeshing(const FaceRange& faces np); } -//todo ip: should I have the overload here? -/* -template -void isotropic_remeshing(const FaceRange& faces - , const double& tol - , const std::pair& edge_len_min_max - , PolygonMesh& pmesh - , const NamedParameters& np = parameters::default_values()) -{ - typedef Adaptive_sizing_field Adaptive_sizing; - Adaptive_sizing sizing(edge_len_min_max, pmesh); - isotropic_remeshing( - faces, - sizing, - pmesh, - np); -} - */ - template +template void tangential_relaxation_with_sizing(const VertexRange& vertices, - TriangleMesh& tm, - const SizingFunction& sizing, - const NamedParameters& np = parameters::default_values()) + TriangleMesh& tm, + const SizingFunction& sizing, + const NamedParameters& np = parameters::default_values()) { typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; @@ -558,10 +558,12 @@ void tangential_relaxation(TriangleMesh& tm, const CGAL_NP_CLASS& np = parameter tangential_relaxation(vertices(tm), tm, np); } -template -void tangential_relaxation_with_sizing(TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) +template +void tangential_relaxation_with_sizing(TriangleMesh& tm, + const SizingFunction& sizing, + const CGAL_NP_CLASS& np = parameters::default_values()) { tangential_relaxation_with_sizing(vertices(tm), tm, sizing, np); } From 73fd72feb9933c6c556b691e747eb32918096df0 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 21 Jun 2023 16:45:42 +0200 Subject: [PATCH 018/124] Add constexpr to differentiate uniform and adaptive fields --- .../Adaptive_sizing_field.h | 2 +- .../Uniform_sizing_field.h | 5 --- .../Isotropic_remeshing/remesh_impl.h | 33 +++++++++++++------ .../CGAL/Polygon_mesh_processing/remesh.h | 1 + 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 7c86e1420499..bbc33ee21630 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -147,7 +147,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field get(m_vertex_sizing_map, target(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); - if(sqlen > sqtarg_len) + if(sqlen > 16./9. * sqtarg_len) return sqlen; else return boost::none; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h index 8fd4bf2334c1..a36c30aa712c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h @@ -56,11 +56,6 @@ class Uniform_sizing_field : public CGAL::Sizing_field } public: - //todo ip: rewrite to remove this? - void calc_sizing_map() const {} - void update_sizing_map(const vertex_descriptor& vnew) const {} - double get_sizing(vertex_descriptor v) const {return 1.;} - boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(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 76e285176346..aec9ca3b5d93 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 @@ -553,7 +553,8 @@ namespace internal { halfedge_added(hnew_opp, status(opposite(he, mesh_))); //todo ip-add: already updating sizing here because of is_too_long checks below - sizing.update_sizing_map(vnew); + if constexpr (!std::is_same>::value) + sizing.update_sizing_map(vnew); //check sub-edges //if it was more than twice the "long" threshold, insert them @@ -1050,18 +1051,29 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); - if (std::is_same>::value) + if constexpr (std::is_same>::value) + { +#ifdef CGAL_PMP_REMESHING_VERBOSE + std::cout << " using tangential relaxation with weights equal to 1"; + std::cout << std::endl; +#endif tangential_relaxation( - vertices(mesh_), - mesh_, - CGAL::parameters::number_of_iterations(nb_iterations) - .vertex_point_map(vpmap_) - .geom_traits(gt_) - .edge_is_constrained_map(constrained_edges_pmap) - .vertex_is_constrained_map(constrained_vertices_pmap) - .relax_constraints(relax_constraints) + vertices(mesh_), + mesh_, + CGAL::parameters::number_of_iterations(nb_iterations) + .vertex_point_map(vpmap_) + .geom_traits(gt_) + .edge_is_constrained_map(constrained_edges_pmap) + .vertex_is_constrained_map(constrained_vertices_pmap) + .relax_constraints(relax_constraints) ); + } else + { +#ifdef CGAL_PMP_REMESHING_VERBOSE + std::cout << " using tangential relaxation weighted with the sizing field"; + std::cout << std::endl; +#endif tangential_relaxation_with_sizing( vertices(mesh_), mesh_, @@ -1073,6 +1085,7 @@ namespace internal { .vertex_is_constrained_map(constrained_vertices_pmap) .relax_constraints(relax_constraints) ); + } CGAL_assertion(!input_mesh_is_valid_ || is_valid_polygon_mesh(mesh_)); 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 787fe54b9930..e751a294445c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -334,6 +334,7 @@ void isotropic_remeshing(const FaceRange& faces t.reset(); t.start(); #endif + if constexpr (!std::is_same>::value) sizing.calc_sizing_map(); for (unsigned int i = 0; i < nb_iterations; ++i) From 0fbcb4175c40bf6561df5213e08753464daf732a Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 29 Jun 2023 19:52:25 +0200 Subject: [PATCH 019/124] Add UI support for adaptive remeshing in Polyhedron demo (WIP) --- .../Polyhedron/Plugins/PMP/CMakeLists.txt | 19 +- .../Plugins/PMP/Isotropic_remeshing_dialog.ui | 225 +++++++++++++----- .../PMP/Isotropic_remeshing_plugin.cpp | 56 ++++- 3 files changed, 227 insertions(+), 73 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/CMakeLists.txt b/Polyhedron/demo/Polyhedron/Plugins/PMP/CMakeLists.txt index 6d5b6cf16d47..354e6641bd62 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/CMakeLists.txt +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/CMakeLists.txt @@ -135,14 +135,19 @@ qt5_wrap_ui( repairUI_FILES RemoveNeedlesDialog.ui SelfSnapDialog.ui) polyhedron_demo_plugin(repair_polyhedron_plugin Repair_polyhedron_plugin ${repairUI_FILES} KEYWORDS PMP) target_link_libraries(repair_polyhedron_plugin PUBLIC scene_points_with_normal_item scene_surface_mesh_item) -qt5_wrap_ui(isotropicRemeshingUI_FILES Isotropic_remeshing_dialog.ui) -polyhedron_demo_plugin(isotropic_remeshing_plugin Isotropic_remeshing_plugin - ${isotropicRemeshingUI_FILES} KEYWORDS PMP) -target_link_libraries(isotropic_remeshing_plugin PUBLIC scene_surface_mesh_item - scene_selection_item) +if(TARGET CGAL::Eigen3_support) + qt5_wrap_ui(isotropicRemeshingUI_FILES Isotropic_remeshing_dialog.ui) + polyhedron_demo_plugin(isotropic_remeshing_plugin Isotropic_remeshing_plugin + ${isotropicRemeshingUI_FILES} KEYWORDS PMP) + target_link_libraries(isotropic_remeshing_plugin PUBLIC scene_surface_mesh_item + scene_selection_item CGAL::Eigen3_support) + + if(TARGET CGAL::TBB_support) + target_link_libraries(isotropic_remeshing_plugin PUBLIC CGAL::TBB_support) + endif() -if(TARGET CGAL::TBB_support) - target_link_libraries(isotropic_remeshing_plugin PUBLIC CGAL::TBB_support) +else() + message(STATUS "NOTICE: Eigen 3.1 (or greater) was not found. Isotropic remeshing plugin will not be available.") endif() polyhedron_demo_plugin(distance_plugin Distance_plugin KEYWORDS PMP) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui index a35ec28d9669..540a52c1bff9 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui @@ -9,8 +9,8 @@ 0 0 - 376 - 369 + 381 + 545 @@ -59,18 +59,117 @@ Isotropic remeshing - - - + + + + + -1 + + + QLayout::SetDefaultConstraint + + + 11 + + + 14 + + + + + Edge sizing + + + + + + + + 0 + 0 + + + + + 168 + 16777215 + + + + + Uniform + + + + + Adaptive + + + + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 24 + + + + + + - Preserve duplicated edges + Error tolerance - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + 0.00 - + + + + Minimum edge length + + + + + + + + + + + 0 @@ -83,24 +182,27 @@ - - + + + + true + - - + + - Target edge length + Number of Smoothing iterations Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + Protect borders/selected edges @@ -110,17 +212,34 @@ - - + + - Number of Smoothing iterations + + + + + + + + Allow 1D smoothing along borders Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + + + + Preserve duplicated edges + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + @@ -133,17 +252,7 @@ - - - - Allow 1D smoothing along borders - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + Number of Main iterations @@ -156,40 +265,17 @@ - - - - - - - - - + + - + Target edge length - - true + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Qt::Vertical - - - QSizePolicy::Maximum - - - - 20 - 40 - - - - - + Qt::ImhNone @@ -205,6 +291,27 @@ + + + + Maximum edge length + + + + + + + 0.00 + + + + + + + 0.00 + + + diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index f1b138c16728..2022de8f9b81 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -825,6 +825,9 @@ public Q_SLOTS: bool edges_only_; double target_length_; + double error_tol_; + double min_length_; + double max_length_; unsigned int nb_iter_; bool protect_; bool smooth_features_; @@ -986,6 +989,32 @@ public Q_SLOTS: } } + void on_edgeSizing_type_combo_box_changed(int index) + { + if (index == 0) + { + ui.edgeLength_label->show(); + ui.edgeLength_dspinbox->show(); + ui.errorTol_label->hide(); + ui.errorTol_edit->hide(); + ui.minEdgeLength_label->hide(); + ui.minEdgeLength_edit->hide(); + ui.maxEdgeLength_label->hide(); + ui.maxEdgeLength_edit->hide(); + } + else if (index == 1) + { + ui.edgeLength_label->hide(); + ui.edgeLength_dspinbox->hide(); + ui.errorTol_label->show(); + ui.errorTol_edit->show(); + ui.minEdgeLength_label->show(); + ui.minEdgeLength_edit->show(); + ui.maxEdgeLength_label->show(); + ui.maxEdgeLength_edit->show(); + } + } + public: void initialize_remeshing_dialog(QDialog* dialog, @@ -1004,6 +1033,8 @@ public Q_SLOTS: connect(ui.protect_checkbox, SIGNAL(clicked(bool)), this, SLOT(update_after_protect_checkbox_click())); connect(ui.splitEdgesOnly_checkbox, SIGNAL(clicked(bool)), this, SLOT(update_after_splitEdgesOnly_click())); + connect(ui.edgeSizing_type_combo_box, SIGNAL(currentIndexChanged(int)), + this, SLOT(on_edgeSizing_type_combo_box_changed(int))); //Set default parameters Scene_interface::Bbox bbox = poly_item != nullptr ? poly_item->bbox() @@ -1025,17 +1056,29 @@ public Q_SLOTS: ui.edgeLength_dspinbox->setValue(0.05 * diago_length); - - std::ostringstream oss; - oss << "Diagonal length of the Bbox of the selection to remesh is "; - oss << diago_length << "." << std::endl; - oss << "Default is 5% of it" << std::endl; - ui.edgeLength_dspinbox->setToolTip(QString::fromStdString(oss.str())); + //todo ip - check and adjust these + ui.errorTol_edit->setValue(0.001 * diago_length); + ui.minEdgeLength_edit->setValue(0.001 * diago_length); + ui.maxEdgeLength_edit->setValue(0.5 * diago_length); + + std::string diag_general_info = "Diagonal length of the Bbox of the selection to remesh is " + + std::to_string(diago_length) + ".\n"; + std::string specific_info; + specific_info = "Default is 5% of it\n"; + ui.edgeLength_dspinbox->setToolTip(QString::fromStdString(diag_general_info + specific_info)); + specific_info = "Default is 0.1% of it\n"; + ui.errorTol_edit->setToolTip(QString::fromStdString(diag_general_info + specific_info)); + specific_info = "Default is 0.1% of it\n"; + ui.minEdgeLength_edit->setToolTip(QString::fromStdString(diag_general_info + specific_info)); + specific_info = "Default is 50% of it\n"; + ui.maxEdgeLength_edit->setToolTip(QString::fromStdString(diag_general_info + specific_info)); ui.nbIterations_spinbox->setSingleStep(1); ui.nbIterations_spinbox->setRange(1/*min*/, 1000/*max*/); ui.nbIterations_spinbox->setValue(1); + ui.edgeSizing_type_combo_box->setCurrentIndex(0); + on_edgeSizing_type_combo_box_changed(0); //todo ip otherwise it shows all remeshing variables ui.protect_checkbox->setChecked(false); ui.smooth1D_checkbox->setChecked(true); @@ -1047,7 +1090,6 @@ public Q_SLOTS: } } - private: QAction* actionIsotropicRemeshing_; Ui::Isotropic_remeshing_dialog ui; From 91216f7875062236e473e8154e7a0924bf88b86f Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 30 Jun 2023 21:15:37 +0200 Subject: [PATCH 020/124] Add adaptive remeshing to Polyhedorn demo, PMP plugin --- .../tangential_relaxation.h | 16 +- .../PMP/Isotropic_remeshing_plugin.cpp | 285 ++++++++++++++---- 2 files changed, 226 insertions(+), 75 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index deb21383c714..d2e955841f11 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -465,16 +465,16 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, const vertex_descriptor v2 = source(h, tm); //todo ip- alt calc - const Vector_3 vec0 = vector(get(vpm, v), get(vpm, v1)); - const Vector_3 vec1 = vector(get(vpm, v), get(vpm, v2)); - const double sqarea = squared_length(cross_product(vec0, vec1)); - const double face_weight = CGAL::approximate_sqrt(sqarea) - / pow(1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2)), 2); +// const Vector_3 vec0 = vector(get(vpm, v), get(vpm, v1)); +// const Vector_3 vec1 = vector(get(vpm, v), get(vpm, v2)); +// const double sqarea = squared_length(cross_product(vec0, vec1)); +// const double face_weight = CGAL::approximate_sqrt(sqarea) +// / pow(1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2)), 2); //todo ip- paper implementation - // const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); - // const double face_weight = tri_area - // / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); + const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); + const double face_weight = tri_area + / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); weight += face_weight; const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 2022de8f9b81..813434a4c6ca 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -358,9 +358,13 @@ public Q_SLOTS: return; } + int edge_sizing_type = ui.edgeSizing_type_combo_box->currentIndex(); bool edges_only = ui.splitEdgesOnly_checkbox->isChecked(); bool preserve_duplicates = ui.preserveDuplicates_checkbox->isChecked(); double target_length = ui.edgeLength_dspinbox->value(); + double error_tol = ui.errorTol_edit->value(); + double min_length = ui.minEdgeLength_edit->value(); + double max_length = ui.maxEdgeLength_edit->value(); unsigned int nb_iter = ui.nbIterations_spinbox->value(); unsigned int nb_smooth = ui.nbSmoothing_spinbox->value(); bool protect = ui.protect_checkbox->isChecked(); @@ -457,28 +461,60 @@ public Q_SLOTS: } } - if (fpmap_valid) - CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) - , target_length - , *selection_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter) - .protect_constraints(protect) - .edge_is_constrained_map(selection_item->constrained_edges_pmap()) - .relax_constraints(smooth_features) - .number_of_relaxation_steps(nb_smooth) - .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) - .face_patch_map(fpmap)); - else - CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) - , target_length - , *selection_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter) - .protect_constraints(protect) - .edge_is_constrained_map(selection_item->constrained_edges_pmap()) - .relax_constraints(smooth_features) - .number_of_relaxation_steps(nb_smooth) - .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) - ); + if (edge_sizing_type == 0) + { + if (fpmap_valid) + CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) + , target_length + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + .face_patch_map(fpmap)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) + , target_length + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + ); + } + else if (edge_sizing_type == 1) + { + std::pair edge_min_max{min_length, max_length}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + , edge_min_max + , *selection_item->polyhedron()); + if (fpmap_valid) + CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) + , adaptive_sizing_field + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + .face_patch_map(fpmap)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) + , adaptive_sizing_field + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + ); + } } else //selected_facets not empty { @@ -507,27 +543,60 @@ public Q_SLOTS: } } - if (fpmap_valid) - CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets - , target_length - , *selection_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter) - .protect_constraints(protect) - .edge_is_constrained_map(selection_item->constrained_edges_pmap()) - .relax_constraints(smooth_features) - .number_of_relaxation_steps(nb_smooth) - .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) - .face_patch_map(fpmap)); - else - CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets - , target_length - , *selection_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter) - .protect_constraints(protect) - .edge_is_constrained_map(selection_item->constrained_edges_pmap()) - .relax_constraints(smooth_features) - .number_of_relaxation_steps(nb_smooth) - .vertex_is_constrained_map(selection_item->constrained_vertices_pmap())); + if (edge_sizing_type == 0) + { + if (fpmap_valid) + CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets + , target_length + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + .face_patch_map(fpmap)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets + , target_length + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + ); + } + else if (edge_sizing_type == 1) + { + std::pair edge_min_max{min_length, max_length}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + , edge_min_max + , *selection_item->polyhedron()); + if (fpmap_valid) + CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets + , adaptive_sizing_field + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + .face_patch_map(fpmap)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets + , adaptive_sizing_field + , *selection_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .edge_is_constrained_map(selection_item->constrained_edges_pmap()) + .relax_constraints(smooth_features) + .number_of_relaxation_steps(nb_smooth) + .vertex_is_constrained_map(selection_item->constrained_vertices_pmap()) + ); + } } } @@ -634,10 +703,40 @@ public Q_SLOTS: } } - if (fpmap_valid) + if (edge_sizing_type == 0) + { + if (fpmap_valid) + CGAL::Polygon_mesh_processing::isotropic_remeshing( + faces(*poly_item->polyhedron()) + , target_length + , *poly_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .number_of_relaxation_steps(nb_smooth) + .edge_is_constrained_map(ecm) + .relax_constraints(smooth_features) + .face_patch_map(fpmap)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing( + faces(*poly_item->polyhedron()) + , target_length + , *poly_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .number_of_relaxation_steps(nb_smooth) + .edge_is_constrained_map(ecm) + .relax_constraints(smooth_features)); + } + else if (edge_sizing_type == 1) + { + std::pair edge_min_max{min_length, max_length}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + , edge_min_max + , *poly_item->polyhedron()); + if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing( faces(*poly_item->polyhedron()) - , target_length + , adaptive_sizing_field , *poly_item->polyhedron() , CGAL::parameters::number_of_iterations(nb_iter) .protect_constraints(protect) @@ -645,16 +744,17 @@ public Q_SLOTS: .edge_is_constrained_map(ecm) .relax_constraints(smooth_features) .face_patch_map(fpmap)); - else - CGAL::Polygon_mesh_processing::isotropic_remeshing( - faces(*poly_item->polyhedron()) - , target_length - , *poly_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter) - .protect_constraints(protect) - .number_of_relaxation_steps(nb_smooth) - .edge_is_constrained_map(ecm) - .relax_constraints(smooth_features)); + else + CGAL::Polygon_mesh_processing::isotropic_remeshing( + faces(*poly_item->polyhedron()) + , adaptive_sizing_field + , *poly_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter) + .protect_constraints(protect) + .number_of_relaxation_steps(nb_smooth) + .edge_is_constrained_map(ecm) + .relax_constraints(smooth_features)); + } //recollect sharp edges for(edge_descriptor e : edges(pmesh)) @@ -687,7 +787,11 @@ public Q_SLOTS: { // Remeshing parameters bool edges_only = false, preserve_duplicates = false; + int edge_sizing_type = 0; double target_length = 0.; + double error_tol = 0.; + double min_length = 0.; + double max_length = 0.; unsigned int nb_iter = 1; bool protect = false; bool smooth_features = true; @@ -723,7 +827,11 @@ public Q_SLOTS: edges_only = ui.splitEdgesOnly_checkbox->isChecked(); preserve_duplicates = ui.preserveDuplicates_checkbox->isChecked(); + edge_sizing_type = ui.edgeSizing_type_combo_box->currentIndex(); target_length = ui.edgeLength_dspinbox->value(); + error_tol = ui.errorTol_edit->value(); + min_length = ui.minEdgeLength_edit->value(); + max_length = ui.maxEdgeLength_edit->value(); nb_iter = ui.nbIterations_spinbox->value(); protect = ui.protect_checkbox->isChecked(); smooth_features = ui.smooth1D_checkbox->isChecked(); @@ -785,8 +893,8 @@ public Q_SLOTS: #else - Remesh_polyhedron_item remesher(edges_only, - target_length, nb_iter, protect, smooth_features); + Remesh_polyhedron_item remesher(edges_only, edge_sizing_type, + target_length, error_tol, min_length, max_length, nb_iter, protect, smooth_features); for(Scene_facegraph_item* poly_item : selection) { QElapsedTimer time; @@ -823,6 +931,7 @@ public Q_SLOTS: typedef boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef boost::graph_traits::face_descriptor face_descriptor; + int edge_sizing_type_; bool edges_only_; double target_length_; double error_tol_; @@ -859,15 +968,34 @@ public Q_SLOTS: std::cout << "Isotropic remeshing of " << poly_item->name().toStdString() << " started..." << std::endl; Scene_polyhedron_selection_item::Is_constrained_map ecm(&edges_to_protect); - CGAL::Polygon_mesh_processing::isotropic_remeshing( - faces(*poly_item->polyhedron()) - , target_length_ - , *poly_item->polyhedron() - , CGAL::parameters::number_of_iterations(nb_iter_) - .protect_constraints(protect_) - .edge_is_constrained_map(ecm) - .face_patch_map(get(CGAL::face_patch_id_t(), *poly_item->polyhedron())) - .relax_constraints(smooth_features_)); + if (edge_sizing_type_ == 0) + { + CGAL::Polygon_mesh_processing::isotropic_remeshing( + faces(*poly_item->polyhedron()) + , target_length_ + , *poly_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter_) + .protect_constraints(protect_) + .edge_is_constrained_map(ecm) + .face_patch_map(get(CGAL::face_patch_id_t(), *poly_item->polyhedron())) + .relax_constraints(smooth_features_)); + } + else + { + std::pair edge_min_max{min_length_, max_length_}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ + , edge_min_max + , *poly_item->polyhedron()); + CGAL::Polygon_mesh_processing::isotropic_remeshing( + faces(*poly_item->polyhedron()) + , target_length_ + , *poly_item->polyhedron() + , CGAL::parameters::number_of_iterations(nb_iter_) + .protect_constraints(protect_) + .edge_is_constrained_map(ecm) + .face_patch_map(get(CGAL::face_patch_id_t(), *poly_item->polyhedron())) + .relax_constraints(smooth_features_)); + } std::cout << "Isotropic remeshing of " << poly_item->name().toStdString() << " done." << std::endl; } @@ -876,12 +1004,20 @@ public Q_SLOTS: public: Remesh_polyhedron_item( const bool edges_only, + const int edge_sizing_type, const double target_length, + const double error_tol, + const double min_length, + const double max_length, const unsigned int nb_iter, const bool protect, const bool smooth_features) : edges_only_(edges_only) + , edge_sizing_type_(edge_sizing_type) , target_length_(target_length) + , error_tol_(error_tol) + , min_length_(min_length) + , max_length_(max_length) , nb_iter_(nb_iter) , protect_(protect) , smooth_features_(smooth_features) @@ -889,7 +1025,11 @@ public Q_SLOTS: Remesh_polyhedron_item(const Remesh_polyhedron_item& remesh) : edges_only_(remesh.edges_only_) + , edge_sizing_type_(remesh.edge_sizing_type_) , target_length_(remesh.target_length_) + , error_tol_(remesh.error_tol_) + , min_length_(remesh.min_length_) + , max_length_(remesh.max_length_) , nb_iter_(remesh.nb_iter_) , protect_(remesh.protect_) , smooth_features_(remesh.smooth_features_) @@ -916,11 +1056,17 @@ public Q_SLOTS: const std::vector& selection, std::map& edges_to_protect, const bool edges_only, + const int edge_sizing_type, const double target_length, + const double error_tol, + const double min_length, + const double max_length, const unsigned int nb_iter, const bool protect, const bool smooth_features) - : RemeshFunctor(edges_only, target_length, nb_iter, protect, smooth_features) + : RemeshFunctor(edges_only, edge_sizing_type, target_length + , error_tol, min_length, max_length + , nb_iter, protect, smooth_features) , selection_(selection), edges_to_protect_(edges_to_protect) {} @@ -973,6 +1119,9 @@ public Q_SLOTS: ui.smooth1D_label->setEnabled(false); ui.smooth1D_checkbox->setEnabled(false); ui.smooth1D_checkbox->setChecked(false); + + ui.edgeSizing_type_combo_box->setCurrentIndex(0); + ui.edgeSizing_type_combo_box->setEnabled(false); } else { @@ -986,6 +1135,8 @@ public Q_SLOTS: ui.smooth1D_label->setEnabled(true); ui.smooth1D_checkbox->setEnabled(true); + + ui.edgeSizing_type_combo_box->setEnabled(true); } } From c8a96328bd48746247b5c48911e1c5090dd60b0f Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 30 Jun 2023 21:26:10 +0200 Subject: [PATCH 021/124] Use C++17 CTAD in example --- .../isotropic_remeshing_with_sizing_example.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index cfbf7bfd9c77..bbb86c89692c 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -27,8 +27,8 @@ int main(int argc, char* argv[]) << " (" << num_faces(mesh) << " faces)..." << std::endl; const double tol = 0.001; - const std::pair edge_min_max{0.001, 0.5}; - PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, mesh); + const std::pair edge_min_max{0.001, 0.5}; + PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, mesh); unsigned int nb_iter = 5; PMP::isotropic_remeshing( From 06db84f717720aafffdadea1c4e5286ebfe15505 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 3 Jul 2023 17:35:02 +0200 Subject: [PATCH 022/124] Fix sizing field calculation --- .../Adaptive_sizing_field.h | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index bbc33ee21630..4a343d5a22ff 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -58,8 +58,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field , const std::pair& edge_len_min_max , PolygonMesh& pmesh) : tol(tol) - , m_sq_short(CGAL::square(edge_len_min_max.first)) - , m_sq_long( CGAL::square(edge_len_min_max.second)) + , m_short(edge_len_min_max.first) + , m_long(edge_len_min_max.second) , m_pmesh(pmesh) { m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); @@ -111,23 +111,23 @@ class Adaptive_sizing_field : public CGAL::Sizing_field // const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature), CGAL::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); - if (vertex_size_sq > m_sq_long) + if (vertex_size_sq > CGAL::square(m_long)) { - put(m_vertex_sizing_map, v, m_sq_long); + put(m_vertex_sizing_map, v, m_long); #ifdef CGAL_PMP_REMESHING_VERBOSE ++oversize; #endif } - else if (vertex_size_sq < m_sq_short) + else if (vertex_size_sq < CGAL::square(m_short)) { - put(m_vertex_sizing_map, v, m_sq_short); + put(m_vertex_sizing_map, v, m_short); #ifdef CGAL_PMP_REMESHING_VERBOSE ++undersize; #endif } else { - put(m_vertex_sizing_map, v, vertex_size_sq); + put(m_vertex_sizing_map, v, CGAL::approximate_sqrt(vertex_size_sq)); #ifdef CGAL_PMP_REMESHING_VERBOSE ++insize; #endif @@ -143,11 +143,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh))); + FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh)))); CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); - if(sqlen > 16./9. * sqtarg_len) + if(sqlen > sqtarg_len) return sqlen; else return boost::none; @@ -157,11 +157,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); - FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, va), - get(m_vertex_sizing_map, vb)); + FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, va), + get(m_vertex_sizing_map, vb))); CGAL_assertion(get(m_vertex_sizing_map, va)); CGAL_assertion(get(m_vertex_sizing_map, vb)); - if (sqlen > 16./9. * sqtarg_len) + if (sqlen > sqtarg_len) return sqlen; else return boost::none; @@ -170,11 +170,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field boost::optional is_too_short(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); - FT sqtarg_len = CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh))); + FT sqtarg_len = CGAL::square(4./5. * CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), + get(m_vertex_sizing_map, target(h, m_pmesh)))); CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); - if (sqlen < 16./25. * sqtarg_len) + if (sqlen < sqtarg_len) return sqlen; else return boost::none; @@ -192,23 +192,23 @@ class Adaptive_sizing_field : public CGAL::Sizing_field { // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split - FT vertex_size_sq = 0; + FT vertex_size = 0; CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh).size() == 2); for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, m_pmesh)) { - vertex_size_sq += get(m_vertex_sizing_map, source(ha, m_pmesh)); + vertex_size += get(m_vertex_sizing_map, source(ha, m_pmesh)); } - vertex_size_sq /= CGAL::halfedges_around_target(v, m_pmesh).size(); + vertex_size /= CGAL::halfedges_around_target(v, m_pmesh).size(); - put(m_vertex_sizing_map, v, vertex_size_sq); + put(m_vertex_sizing_map, v, vertex_size); } //todo ip: is_protected_constraint_too_long() from PR private: const FT tol; - const FT m_sq_short; - const FT m_sq_long; + const FT m_short; + const FT m_long; PolygonMesh& m_pmesh; VertexSizingMap m_vertex_sizing_map; }; From 677bb04ee8c99035f4bc2f6dc40db9182fdd8e1a Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 5 Jul 2023 11:34:14 +0200 Subject: [PATCH 023/124] (WIP) figuring out FaceRange curvature calculation --- .../Adaptive_sizing_field.h | 70 ++++++++++--------- .../CGAL/Polygon_mesh_processing/remesh.h | 3 +- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 4a343d5a22ff..3cec50bdd17a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -37,33 +37,28 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename Base::Point_3 Point_3; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; + //todo ip: send this over to Sizing_field to be consistent + typedef typename boost::graph_traits::face_descriptor face_descriptor; typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map::type VertexSizingMap; + Vertex_property_tag>::type Vertex_sizing_map; - //todo ip: set a property map that can calculate curvature in one go. I think I'm generating constant maps (without put) - // try 1 typedef Principal_curvatures_and_directions Principal_curvatures; -// typedef Constant_property_map Vertex_curvature_map; - - // try 2 - typedef Constant_property_map> Default_principal_map; - typedef typename internal_np::Lookup_named_param_def::type - Vertex_curvature_map; + typedef typename CGAL::dynamic_vertex_property_t Vertex_curvature_tag; + typedef typename boost::property_map, + Vertex_curvature_tag>::type Vertex_curvature_map; Adaptive_sizing_field(const double tol , const std::pair& edge_len_min_max , PolygonMesh& pmesh) - : tol(tol) - , m_short(edge_len_min_max.first) - , m_long(edge_len_min_max.second) - , m_pmesh(pmesh) - { - m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); - } + : tol(tol) + , m_short(edge_len_min_max.first) + , m_long(edge_len_min_max.second) + , m_pmesh(pmesh) + { + m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); + } private: FT sqlength(const vertex_descriptor va, @@ -85,7 +80,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return get(m_vertex_sizing_map, v); } - void calc_sizing_map() + template + void calc_sizing_map(const FaceRange& faces) { #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; @@ -94,22 +90,32 @@ class Adaptive_sizing_field : public CGAL::Sizing_field std::cout << "Calculating sizing field..." << std::endl; #endif - //todo ip: how to make this work? -// Vertex_curvature_map vertex_curvature_map; + ////// IP: How to sort this out? + ///// FaceRange->expand->Face_filtered_graph->use ffg onwards +// CGAL::make_boolean_property_map(faces); // IP: this does not compile + std::vector selection; + auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); + for (face_descriptor f : faces) + put(is_selected, f, false); + + CGAL::expand_face_selection(selection, m_pmesh, 1, is_selected, std::back_inserter(selection)); + // IP: expand_face_selection is not happy with ffg either + + CGAL::Face_filtered_graph ffg(m_pmesh, selection); + /////// + + Vertex_curvature_map vertex_curvature_map; + vertex_curvature_map = get(Vertex_curvature_tag(), ffg); - //todo ip: temp workaround - auto vertex_curvature_map = - m_pmesh.template add_property_map>("v:curvature_map").first; - interpolated_corrected_principal_curvatures_and_directions(m_pmesh - , vertex_curvature_map); + interpolated_corrected_principal_curvatures_and_directions(ffg + , vertex_curvature_map); - // calculate square vertex sizing field (L(x_i))^2 from curvature field - for(vertex_descriptor v : vertices(m_pmesh)) + // calculate vertex sizing field L(x_i) from curvature field + for(vertex_descriptor v : vertices(ffg)) { auto vertex_curv = get(vertex_curvature_map, v); - //todo ip: alt solution - calculate curvature per vertex -// const Principal_curvatures vertex_curv = interpolated_corrected_principal_curvatures_and_directions_one_vertex(m_pmesh, v); - const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature), CGAL::abs(vertex_curv.min_curvature)); + const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature) + , CGAL::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); if (vertex_size_sq > CGAL::square(m_long)) { @@ -210,7 +216,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field const FT m_short; const FT m_long; PolygonMesh& m_pmesh; - VertexSizingMap m_vertex_sizing_map; + Vertex_sizing_map m_vertex_sizing_map; }; }//end namespace Polygon_mesh_processing 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 e751a294445c..0ea55b8bb59b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -334,8 +334,9 @@ void isotropic_remeshing(const FaceRange& faces t.reset(); t.start(); #endif + //todo ip: move calc_sizing_map to the sizing function constructor if constexpr (!std::is_same>::value) - sizing.calc_sizing_map(); + sizing.calc_sizing_map(faces); for (unsigned int i = 0; i < nb_iterations; ++i) { From 63e31805175af4407b7776ed0dcfcbfa66b7b484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 5 Jul 2023 13:10:28 +0200 Subject: [PATCH 024/124] use vector option for selection --- .../Isotropic_remeshing/Adaptive_sizing_field.h | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 3cec50bdd17a..91524857764d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -81,7 +81,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } template - void calc_sizing_map(const FaceRange& faces) + void calc_sizing_map(const FaceRange& faces_) { #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; @@ -90,16 +90,12 @@ class Adaptive_sizing_field : public CGAL::Sizing_field std::cout << "Calculating sizing field..." << std::endl; #endif - ////// IP: How to sort this out? ///// FaceRange->expand->Face_filtered_graph->use ffg onwards -// CGAL::make_boolean_property_map(faces); // IP: this does not compile - std::vector selection; + std::vector selection(faces_.begin(), faces_.end()); auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); - for (face_descriptor f : faces) + for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); - CGAL::expand_face_selection(selection, m_pmesh, 1, is_selected, std::back_inserter(selection)); - // IP: expand_face_selection is not happy with ffg either CGAL::Face_filtered_graph ffg(m_pmesh, selection); /////// From 4a5283b22e5ec0b0dcfd4f085d1087ee104cd527 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 5 Jul 2023 20:29:59 +0200 Subject: [PATCH 025/124] Change selection option to set --- .../Adaptive_sizing_field.h | 23 +++++++------------ .../Isotropic_remeshing/Sizing_field.h | 1 + 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 91524857764d..450bb4c1ea4d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -35,10 +35,9 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename Base::K K; typedef typename Base::FT FT; typedef typename Base::Point_3 Point_3; + typedef typename Base::face_descriptor face_descriptor; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; - //todo ip: send this over to Sizing_field to be consistent - typedef typename boost::graph_traits::face_descriptor face_descriptor; typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map } template - void calc_sizing_map(const FaceRange& faces_) + void calc_sizing_map(const FaceRange& face_range) { #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; @@ -90,22 +89,16 @@ class Adaptive_sizing_field : public CGAL::Sizing_field std::cout << "Calculating sizing field..." << std::endl; #endif - ///// FaceRange->expand->Face_filtered_graph->use ffg onwards - std::vector selection(faces_.begin(), faces_.end()); - auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); - for (face_descriptor f : faces(m_pmesh)) - put(is_selected, f, false); - CGAL::expand_face_selection(selection, m_pmesh, 1, is_selected, std::back_inserter(selection)); - + // expand face selection for curvature calculation + std::set selection(face_range.begin(), face_range.end()); + CGAL::expand_face_selection(selection, m_pmesh, 1 + , make_boolean_property_map(selection), Emptyset_iterator()); CGAL::Face_filtered_graph ffg(m_pmesh, selection); - /////// - - Vertex_curvature_map vertex_curvature_map; - vertex_curvature_map = get(Vertex_curvature_tag(), ffg); + // calculate curvature + Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), ffg); interpolated_corrected_principal_curvatures_and_directions(ffg , vertex_curvature_map); - // calculate vertex sizing field L(x_i) from curvature field for(vertex_descriptor v : vertices(ffg)) { diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index 359a9fc6b562..3640e14c092d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -33,6 +33,7 @@ class Sizing_field public: typedef typename CGAL::Kernel_traits::Kernel K; + typedef typename boost::graph_traits::face_descriptor face_descriptor; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef Point Point_3; From a61ebb545e5076d0b291ebc9d5f6a50d64bf0516 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 6 Jul 2023 22:35:33 +0200 Subject: [PATCH 026/124] Change face subset back to working example with vector --- .../internal/Isotropic_remeshing/Adaptive_sizing_field.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 450bb4c1ea4d..0471f4fd3c0b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -90,16 +90,19 @@ class Adaptive_sizing_field : public CGAL::Sizing_field #endif // expand face selection for curvature calculation - std::set selection(face_range.begin(), face_range.end()); + std::vector selection(face_range.begin(), face_range.end()); + auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); + for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); + for (face_descriptor f : face_range) put(is_selected, f, true); CGAL::expand_face_selection(selection, m_pmesh, 1 - , make_boolean_property_map(selection), Emptyset_iterator()); + , is_selected, std::back_inserter(selection)); CGAL::Face_filtered_graph ffg(m_pmesh, selection); // calculate curvature Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), ffg); interpolated_corrected_principal_curvatures_and_directions(ffg , vertex_curvature_map); - // calculate vertex sizing field L(x_i) from curvature field + // calculate vertex sizing field L(x_i) from the curvature field for(vertex_descriptor v : vertices(ffg)) { auto vertex_curv = get(vertex_curvature_map, v); From 99661dfd73bd7c25d1f21cef339532830c1e8cd0 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 7 Jul 2023 18:33:03 +0200 Subject: [PATCH 027/124] Choose betwen curvature calc for selection and whole mesh --- .../Adaptive_sizing_field.h | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 0471f4fd3c0b..0e34893d7ef4 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -81,6 +81,29 @@ class Adaptive_sizing_field : public CGAL::Sizing_field template void calc_sizing_map(const FaceRange& face_range) + { + if (face_range.size() == faces(m_pmesh).size()) + { + // calculate curvature from the whole mesh + calc_sizing_map(m_pmesh); + } + else + { + // expand face selection and calculate curvature from it + std::vector selection(face_range.begin(), face_range.end()); + auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); + for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); + for (face_descriptor f : face_range) put(is_selected, f, true); + CGAL::expand_face_selection(selection, m_pmesh, 1 + , is_selected, std::back_inserter(selection)); + CGAL::Face_filtered_graph ffg(m_pmesh, selection); + + calc_sizing_map(ffg); + } + } + + template + void calc_sizing_map(FaceGraph& face_graph) { #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; @@ -89,21 +112,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field std::cout << "Calculating sizing field..." << std::endl; #endif - // expand face selection for curvature calculation - std::vector selection(face_range.begin(), face_range.end()); - auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); - for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); - for (face_descriptor f : face_range) put(is_selected, f, true); - CGAL::expand_face_selection(selection, m_pmesh, 1 - , is_selected, std::back_inserter(selection)); - CGAL::Face_filtered_graph ffg(m_pmesh, selection); - - // calculate curvature - Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), ffg); - interpolated_corrected_principal_curvatures_and_directions(ffg + Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), face_graph); + interpolated_corrected_principal_curvatures_and_directions(face_graph , vertex_curvature_map); // calculate vertex sizing field L(x_i) from the curvature field - for(vertex_descriptor v : vertices(ffg)) + for(vertex_descriptor v : vertices(face_graph)) { auto vertex_curv = get(vertex_curvature_map, v); const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature) @@ -133,8 +146,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " done (" << insize << " from curvature, " - << oversize << " set to max, " - << undersize << " set to min)" << std::endl; + << oversize << " set to max, " + << undersize << " set to min)" << std::endl; #endif } From 573cc53e0a32147dbdd48486b9c4c9b53f6934df Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 10 Jul 2023 21:28:06 +0200 Subject: [PATCH 028/124] Move curvature map typedef to function --- .../Adaptive_sizing_field.h | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 0e34893d7ef4..071bc761414a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -43,21 +43,16 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename boost::property_map::type Vertex_sizing_map; - typedef Principal_curvatures_and_directions Principal_curvatures; - typedef typename CGAL::dynamic_vertex_property_t Vertex_curvature_tag; - typedef typename boost::property_map, - Vertex_curvature_tag>::type Vertex_curvature_map; - - Adaptive_sizing_field(const double tol - , const std::pair& edge_len_min_max - , PolygonMesh& pmesh) - : tol(tol) - , m_short(edge_len_min_max.first) - , m_long(edge_len_min_max.second) - , m_pmesh(pmesh) - { - m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); - } + Adaptive_sizing_field(const double tol + , const std::pair& edge_len_min_max + , PolygonMesh& pmesh) + : tol(tol) + , m_short(edge_len_min_max.first) + , m_long(edge_len_min_max.second) + , m_pmesh(pmesh) + { + m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); + } private: FT sqlength(const vertex_descriptor va, @@ -105,6 +100,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field template void calc_sizing_map(FaceGraph& face_graph) { + typedef Principal_curvatures_and_directions Principal_curvatures; + typedef typename CGAL::dynamic_vertex_property_t Vertex_curvature_tag; + typedef typename boost::property_map::type Vertex_curvature_map; + #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; int undersize = 0; @@ -145,7 +145,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } } #ifdef CGAL_PMP_REMESHING_VERBOSE - std::cout << " done (" << insize << " from curvature, " + std::cout << " done (" << insize << " from curvature, " << oversize << " set to max, " << undersize << " set to min)" << std::endl; #endif From 1c597a07cf5ad07742f1246f4a20bb05e5a8199b Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 11 Jul 2023 09:33:37 +0200 Subject: [PATCH 029/124] Move sizing map calculation to constructor --- ...sotropic_remeshing_with_sizing_example.cpp | 2 +- .../Adaptive_sizing_field.h | 69 ++++++++++++------- .../Isotropic_remeshing/remesh_impl.h | 2 +- .../CGAL/Polygon_mesh_processing/remesh.h | 6 -- .../PMP/Isotropic_remeshing_plugin.cpp | 4 ++ 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index bbb86c89692c..e0564d27178d 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -28,7 +28,7 @@ int main(int argc, char* argv[]) const double tol = 0.001; const std::pair edge_min_max{0.001, 0.5}; - PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, mesh); + PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, faces(mesh), mesh); unsigned int nb_iter = 5; PMP::isotropic_remeshing( diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index 071bc761414a..dc14ae91d119 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -43,8 +43,10 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename boost::property_map::type Vertex_sizing_map; + template Adaptive_sizing_field(const double tol , const std::pair& edge_len_min_max + , const FaceRange& face_range , PolygonMesh& pmesh) : tol(tol) , m_short(edge_len_min_max.first) @@ -52,31 +54,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field , m_pmesh(pmesh) { m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); - } - -private: - FT sqlength(const vertex_descriptor va, - const vertex_descriptor vb) const - { - typename boost::property_map::const_type - vpmap = get(CGAL::vertex_point, m_pmesh); - return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); - } - - FT sqlength(const halfedge_descriptor& h) const - { - return sqlength(target(h, m_pmesh), source(h, m_pmesh)); - } - -public: - FT get_sizing(const vertex_descriptor v) const { - CGAL_assertion(get(m_vertex_sizing_map, v)); - return get(m_vertex_sizing_map, v); - } - template - void calc_sizing_map(const FaceRange& face_range) - { if (face_range.size() == faces(m_pmesh).size()) { // calculate curvature from the whole mesh @@ -97,6 +75,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field } } +private: template void calc_sizing_map(FaceGraph& face_graph) { @@ -151,6 +130,48 @@ class Adaptive_sizing_field : public CGAL::Sizing_field #endif } + FT sqlength(const vertex_descriptor va, + const vertex_descriptor vb) const + { + typename boost::property_map::const_type + vpmap = get(CGAL::vertex_point, m_pmesh); + return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); + } + + FT sqlength(const halfedge_descriptor& h) const + { + return sqlength(target(h, m_pmesh), source(h, m_pmesh)); + } + +public: + FT get_sizing(const vertex_descriptor v) const { + CGAL_assertion(get(m_vertex_sizing_map, v)); + return get(m_vertex_sizing_map, v); + } + + template + void calc_sizing_map(const FaceRange& face_range) + { + if (face_range.size() == faces(m_pmesh).size()) + { + // calculate curvature from the whole mesh + calc_sizing_map(m_pmesh); + } + else + { + // expand face selection and calculate curvature from it + std::vector selection(face_range.begin(), face_range.end()); + auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); + for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); + for (face_descriptor f : face_range) put(is_selected, f, true); + CGAL::expand_face_selection(selection, m_pmesh, 1 + , is_selected, std::back_inserter(selection)); + CGAL::Face_filtered_graph ffg(m_pmesh, selection); + + calc_sizing_map(ffg); + } + } + boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(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 aec9ca3b5d93..0e772b47cb58 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 @@ -553,7 +553,7 @@ namespace internal { halfedge_added(hnew_opp, status(opposite(he, mesh_))); //todo ip-add: already updating sizing here because of is_too_long checks below - if constexpr (!std::is_same>::value) + if constexpr (!std::is_same_v>) sizing.update_sizing_map(vnew); //check sub-edges 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 0ea55b8bb59b..ec443f179e58 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -334,18 +334,12 @@ void isotropic_remeshing(const FaceRange& faces t.reset(); t.start(); #endif - //todo ip: move calc_sizing_map to the sizing function constructor - if constexpr (!std::is_same>::value) - sizing.calc_sizing_map(faces); - for (unsigned int i = 0; i < nb_iterations; ++i) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " * Iteration " << (i + 1) << " *" << std::endl; #endif -// if (i < 2) -// sizing.calc_sizing_map(); if(do_split) remesher.split_long_edges(sizing); if(do_collapse) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 813434a4c6ca..679d2518666d 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -491,6 +491,7 @@ public Q_SLOTS: std::pair edge_min_max{min_length, max_length}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max + , faces(*selection_item->polyhedron()) , *selection_item->polyhedron()); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) @@ -573,6 +574,7 @@ public Q_SLOTS: std::pair edge_min_max{min_length, max_length}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max + , faces(*selection_item->polyhedron()) , *selection_item->polyhedron()); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets @@ -732,6 +734,7 @@ public Q_SLOTS: std::pair edge_min_max{min_length, max_length}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max + , faces(*poly_item->polyhedron()) , *poly_item->polyhedron()); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing( @@ -985,6 +988,7 @@ public Q_SLOTS: std::pair edge_min_max{min_length_, max_length_}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ , edge_min_max + , faces(*poly_item->polyhedron()) , *poly_item->polyhedron()); CGAL::Polygon_mesh_processing::isotropic_remeshing( faces(*poly_item->polyhedron()) From cc0c46c7ef331af28422e37de79785473ceb8052 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 11 Jul 2023 10:08:21 +0200 Subject: [PATCH 030/124] Fix polyhedron demo with TBB --- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 679d2518666d..86d7844949a3 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -890,7 +890,11 @@ public Q_SLOTS: tbb::parallel_for( tbb::blocked_range(0, selection.size()), Remesh_polyhedron_item_for_parallel_for( - selection, edges_to_protect, edges_only, target_length, nb_iter, protect, smooth_features)); + selection, edges_to_protect, edges_only + , edge_sizing_type, target_length, error_tol + , min_length , max_length, nb_iter + , protect, smooth_features) + ); total_time = time.elapsed(); From 4b1b04eb8d3542b3846e6d0bf091b34e3fff6173 Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Tue, 1 Aug 2023 11:09:28 +0200 Subject: [PATCH 031/124] fix compilation --- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 86d7844949a3..406c032f6feb 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -489,7 +489,7 @@ public Q_SLOTS: else if (edge_sizing_type == 1) { std::pair edge_min_max{min_length, max_length}; - PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*selection_item->polyhedron()) , *selection_item->polyhedron()); @@ -572,7 +572,7 @@ public Q_SLOTS: else if (edge_sizing_type == 1) { std::pair edge_min_max{min_length, max_length}; - PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*selection_item->polyhedron()) , *selection_item->polyhedron()); @@ -732,7 +732,7 @@ public Q_SLOTS: else if (edge_sizing_type == 1) { std::pair edge_min_max{min_length, max_length}; - PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*poly_item->polyhedron()) , *poly_item->polyhedron()); @@ -990,7 +990,7 @@ public Q_SLOTS: else { std::pair edge_min_max{min_length_, max_length_}; - PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ , edge_min_max , faces(*poly_item->polyhedron()) , *poly_item->polyhedron()); From 58e0bf13a685e5f0d88685a69a5e34350755b8bc Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Wed, 2 Aug 2023 10:41:21 +0200 Subject: [PATCH 032/124] reintroduce mean curvature and gaussian curvature in "display properties" broken for now --- .../Plugins/Display/Display_property.ui | 26 +++ .../Display/Display_property_plugin.cpp | 159 +++++++++++++++++- 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui index 656ec4827c55..d1e5e795e711 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui @@ -180,6 +180,32 @@ + + + + 0 + + + 100 + + + true + + + Qt::Horizontal + + + QSlider::TicksAbove + + + + + + + Expanding Radius: 0 + + + diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp index 994dcd2e8d9b..5b212d5a4703 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -85,6 +87,9 @@ class Display_property_plugin double gI = 1.; double bI = 0.; + double expand_radius = 0; + double maxEdgeLength = -1; + Color_ramp color_ramp; std::vector color_map; QPixmap legend; @@ -98,6 +103,11 @@ class Display_property_plugin MIN_VALUE, MAX_VALUE }; + enum CurvatureType + { + MEAN_CURVATURE, + GAUSSIAN_CURVATURE, + }; public: bool applicable(QAction*) const Q_DECL_OVERRIDE @@ -196,6 +206,8 @@ class Display_property_plugin this, &Display_property_plugin::on_zoomToMinButton_pressed); connect(dock_widget->zoomToMaxButton, &QPushButton::pressed, this, &Display_property_plugin::on_zoomToMaxButton_pressed); + connect(dock_widget->expandingRadiusSlider, SIGNAL(valueChanged(int)), + this, SLOT(setExpandingRadius(int))); } private Q_SLOTS: @@ -252,6 +264,20 @@ private Q_SLOTS: dock_widget->maxBox->setRange(-1000, 1000); dock_widget->maxBox->setValue(0); } + else if (property_name == "Interpolated Corrected Mean Curvature") + { + dock_widget->minBox->setRange(-99999999, 99999999); + dock_widget->minBox->setValue(0); + dock_widget->maxBox->setRange(-99999999, 99999999); + dock_widget->maxBox->setValue(0); + } + else if (property_name == "Interpolated Corrected Gaussian Curvature") + { + dock_widget->minBox->setRange(-99999999, 99999999); + dock_widget->minBox->setValue(0); + dock_widget->maxBox->setRange(-99999999, 99999999); + dock_widget->maxBox->setValue(0); + } else { dock_widget->minBox->setRange(-99999999, 99999999); @@ -432,11 +458,15 @@ private Q_SLOTS: dock_widget->propertyBox->addItems({"Smallest Angle Per Face", "Largest Angle Per Face", "Scaled Jacobian", - "Face Area"}); + "Face Area", + "Interpolated Corrected Mean Curvature", + "Interpolated Corrected Gaussian Curvature"}); property_simplex_types = { Property_simplex_type::FACE, Property_simplex_type::FACE, Property_simplex_type::FACE, - Property_simplex_type::FACE }; + Property_simplex_type::FACE, + Property_simplex_type::VERTEX, + Property_simplex_type::VERTEX }; detectSMScalarProperties(*(sm_item->face_graph())); } else if(ps_item) @@ -527,6 +557,16 @@ private Q_SLOTS: { displayArea(sm_item); } + else if(property_name == "Interpolated Corrected Mean Curvature") + { + displayInterpolatedCurvatureMeasure(sm_item, MEAN_CURVATURE); + sm_item->setRenderingMode(Gouraud); + } + else if(property_name == "Interpolated Corrected Gaussian Curvature") + { + displayInterpolatedCurvatureMeasure(sm_item, GAUSSIAN_CURVATURE); + sm_item->setRenderingMode(Gouraud); + } else { const int property_index = dock_widget->propertyBox->currentIndex(); @@ -629,6 +669,8 @@ private Q_SLOTS: removeDisplayPluginProperty(item, "f:display_plugin_largest_angle"); removeDisplayPluginProperty(item, "f:display_plugin_scaled_jacobian"); removeDisplayPluginProperty(item, "f:display_plugin_area"); + removeDisplayPluginProperty(item, "v:interpolated_corrected_mean_curvature"); + removeDisplayPluginProperty(item, "v:interpolated_corrected_Gaussian_curvature"); } void displayExtremumAnglePerFace(Scene_surface_mesh_item* sm_item, @@ -809,6 +851,119 @@ private Q_SLOTS: displaySMProperty("f:display_plugin_area", *sm); } + void setExpandingRadius(int val_int) + { + double sliderMin = dock_widget->expandingRadiusSlider->minimum(); + double sliderMax = dock_widget->expandingRadiusSlider->maximum() - sliderMin; + double val = val_int - sliderMin; + sliderMin = 0; + + SMesh& smesh = *(qobject_cast(scene->item(scene->mainSelectionIndex())))->face_graph(); + + auto vpm = get(CGAL::vertex_point, smesh); + + if (maxEdgeLength < 0) + { + auto edge_range = CGAL::edges(smesh); + + if (edge_range.begin() == edge_range.end()) + { + expand_radius = 0; + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); + return; + } + + auto edge_reference = std::max_element(edge_range.begin(), edge_range.end(), [&, vpm, smesh](auto l, auto r) { + auto res = EPICK().compare_squared_distance_3_object()( + get(vpm, source((l), smesh)), + get(vpm, target((l), smesh)), + get(vpm, source((r), smesh)), + get(vpm, target((r), smesh))); + return res == CGAL::SMALLER; + }); + + // if edge_reference is not derefrenceble + if (edge_reference == edge_range.end()) + { + expand_radius = 0; + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); + return; + } + + maxEdgeLength = sqrt( + (get(vpm, source((*edge_reference), smesh)) - get(vpm, target((*edge_reference), smesh))) + .squared_length() + ); + + } + + double outMax = 5 * maxEdgeLength, base = 1.2; + + expand_radius = (pow(base, val) - 1) * outMax / (pow(base, sliderMax) - 1); + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); + + } + + void displayInterpolatedCurvatureMeasure(Scene_surface_mesh_item* item, CurvatureType mu_index) + { + namespace PMP = CGAL::Polygon_mesh_processing; + + if (mu_index != MEAN_CURVATURE && mu_index != GAUSSIAN_CURVATURE) return; + + std::string tied_string = (mu_index == MEAN_CURVATURE)? + "v:interpolated_corrected_mean_curvature": "v:interpolated_corrected_Gaussian_curvature"; + SMesh& smesh = *item->face_graph(); + + const auto vnm = smesh.property_map("v:normal_before_perturbation").first; + const bool vnm_exists = smesh.property_map("v:normal_before_perturbation").second; + + //compute once and store the value per vertex + bool non_init; + SMesh::Property_map mu_i_map; + std::tie(mu_i_map, non_init) = + smesh.add_property_map(tied_string, 0); + if (non_init) + { + if (vnm_exists) { + if (mu_index == MEAN_CURVATURE) + PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius).vertex_normal_map(vnm)); + else + PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius).vertex_normal_map(vnm)); + } + else { + if (mu_index == MEAN_CURVATURE) + PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); + else + PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); + } + double res_min = ARBITRARY_DBL_MAX, + res_max = -ARBITRARY_DBL_MAX; + SMesh::Vertex_index min_index, max_index; + for (SMesh::Vertex_index v : vertices(smesh)) + { + if (mu_i_map[v] > res_max) + { + res_max = mu_i_map[v]; + max_index = v; + } + if (mu_i_map[v] < res_min) + { + res_min = mu_i_map[v]; + min_index = v; + } + } +// if (mu_index == MEAN_CURVATURE){ +// mean_curvature_max[item] = std::make_pair(res_max, max_index); +// mean_curvature_min[item] = std::make_pair(res_min, min_index); +// } +// else { +// gaussian_curvature_max[item] = std::make_pair(res_max, max_index); +// gaussian_curvature_min[item] = std::make_pair(res_min, min_index); +// } + } +// treat_sm_property(tied_string, item->face_graph()); + } + private: template bool call_on_PS_property(const std::string& name, From 41bddc72d6f3feebad57682345b80fcbe289eca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mael=20Rouxel-Labb=C3=A9?= Date: Wed, 2 Aug 2023 12:29:29 +0200 Subject: [PATCH 033/124] Display plugin fixes --- .../Display/Display_property_plugin.cpp | 141 +++++++++--------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp index 5b212d5a4703..037d5bb4982b 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp @@ -47,6 +47,8 @@ #define ARBITRARY_DBL_MIN 1.0E-17 #define ARBITRARY_DBL_MAX 1.0E+17 +namespace PMP = CGAL::Polygon_mesh_processing; + using namespace CGAL::Three; Viewer_interface* (&getActiveViewer)() = Three::activeViewer; @@ -206,6 +208,7 @@ class Display_property_plugin this, &Display_property_plugin::on_zoomToMinButton_pressed); connect(dock_widget->zoomToMaxButton, &QPushButton::pressed, this, &Display_property_plugin::on_zoomToMaxButton_pressed); + connect(dock_widget->expandingRadiusSlider, SIGNAL(valueChanged(int)), this, SLOT(setExpandingRadius(int))); } @@ -669,8 +672,8 @@ private Q_SLOTS: removeDisplayPluginProperty(item, "f:display_plugin_largest_angle"); removeDisplayPluginProperty(item, "f:display_plugin_scaled_jacobian"); removeDisplayPluginProperty(item, "f:display_plugin_area"); - removeDisplayPluginProperty(item, "v:interpolated_corrected_mean_curvature"); - removeDisplayPluginProperty(item, "v:interpolated_corrected_Gaussian_curvature"); + removeDisplayPluginProperty(item, "v:display_plugin_interpolated_corrected_mean_curvature"); + removeDisplayPluginProperty(item, "v:display_plugin_interpolated_corrected_Gaussian_curvature"); } void displayExtremumAnglePerFace(Scene_surface_mesh_item* sm_item, @@ -759,7 +762,7 @@ private Q_SLOTS: halfedge_descriptor local_border_h = opposite(halfedge(local_f, local_smesh), local_smesh); CGAL_assertion(is_border(local_border_h, local_smesh)); - CGAL::Polygon_mesh_processing::triangulate_faces(local_smesh); + PMP::triangulate_faces(local_smesh); double extremum_angle_in_face = ARBITRARY_DBL_MAX; halfedge_descriptor local_border_end_h = local_border_h; @@ -812,7 +815,7 @@ private Q_SLOTS: const SMesh& mesh) const { if(CGAL::is_triangle(halfedge(f, mesh), mesh)) - return CGAL::Polygon_mesh_processing::face_area(f, mesh); + return PMP::face_area(f, mesh); auto vpm = get(boost::vertex_point, mesh); @@ -828,8 +831,8 @@ private Q_SLOTS: } CGAL::Euler::add_face(local_vertices, local_smesh); - CGAL::Polygon_mesh_processing::triangulate_faces(local_smesh); - return CGAL::Polygon_mesh_processing::area(local_smesh); + PMP::triangulate_faces(local_smesh); + return PMP::area(local_smesh); } void displayArea(Scene_surface_mesh_item* sm_item) @@ -851,117 +854,105 @@ private Q_SLOTS: displaySMProperty("f:display_plugin_area", *sm); } - void setExpandingRadius(int val_int) + void setExpandingRadius(const int val_int) { double sliderMin = dock_widget->expandingRadiusSlider->minimum(); double sliderMax = dock_widget->expandingRadiusSlider->maximum() - sliderMin; - double val = val_int - sliderMin; + double val = val_int - sliderMin; sliderMin = 0; - SMesh& smesh = *(qobject_cast(scene->item(scene->mainSelectionIndex())))->face_graph(); + Scene_item* item = scene->item(scene->mainSelectionIndex()); + Scene_surface_mesh_item* sm_item = qobject_cast(item); + if(sm_item == nullptr) + return; + + SMesh& smesh = *(sm_item->face_graph()); auto vpm = get(CGAL::vertex_point, smesh); - if (maxEdgeLength < 0) + // @todo use the upcoming PMP::longest_edge + if(maxEdgeLength < 0) { auto edge_range = CGAL::edges(smesh); - if (edge_range.begin() == edge_range.end()) + if(num_edges(smesh) == 0) { expand_radius = 0; dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); return; } - auto edge_reference = std::max_element(edge_range.begin(), edge_range.end(), [&, vpm, smesh](auto l, auto r) { - auto res = EPICK().compare_squared_distance_3_object()( - get(vpm, source((l), smesh)), - get(vpm, target((l), smesh)), - get(vpm, source((r), smesh)), - get(vpm, target((r), smesh))); - return res == CGAL::SMALLER; - }); - - // if edge_reference is not derefrenceble - if (edge_reference == edge_range.end()) - { - expand_radius = 0; - dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); - return; - } + auto eit = std::max_element(edge_range.begin(), edge_range.end(), + [&, vpm, smesh](auto l, auto r) + { + auto res = EPICK().compare_squared_distance_3_object()( + get(vpm, source((l), smesh)), + get(vpm, target((l), smesh)), + get(vpm, source((r), smesh)), + get(vpm, target((r), smesh))); + return (res == CGAL::SMALLER); + }); - maxEdgeLength = sqrt( - (get(vpm, source((*edge_reference), smesh)) - get(vpm, target((*edge_reference), smesh))) - .squared_length() - ); + CGAL_assertion(eit != edge_range.end()); + maxEdgeLength = PMP::edge_length(*eit, smesh); } double outMax = 5 * maxEdgeLength, base = 1.2; expand_radius = (pow(base, val) - 1) * outMax / (pow(base, sliderMax) - 1); dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); - } - void displayInterpolatedCurvatureMeasure(Scene_surface_mesh_item* item, CurvatureType mu_index) + void displayInterpolatedCurvatureMeasure(Scene_surface_mesh_item* item, + CurvatureType mu_index) { - namespace PMP = CGAL::Polygon_mesh_processing; + if(mu_index != MEAN_CURVATURE && mu_index != GAUSSIAN_CURVATURE) + return; - if (mu_index != MEAN_CURVATURE && mu_index != GAUSSIAN_CURVATURE) return; + std::string tied_string = (mu_index == MEAN_CURVATURE) ? "v:display_plugin_interpolated_corrected_mean_curvature" + : "v:display_plugin_interpolated_corrected_Gaussian_curvature"; - std::string tied_string = (mu_index == MEAN_CURVATURE)? - "v:interpolated_corrected_mean_curvature": "v:interpolated_corrected_Gaussian_curvature"; SMesh& smesh = *item->face_graph(); const auto vnm = smesh.property_map("v:normal_before_perturbation").first; const bool vnm_exists = smesh.property_map("v:normal_before_perturbation").second; - //compute once and store the value per vertex + // compute once and store the value per vertex bool non_init; SMesh::Property_map mu_i_map; - std::tie(mu_i_map, non_init) = - smesh.add_property_map(tied_string, 0); - if (non_init) + std::tie(mu_i_map, non_init) = smesh.add_property_map(tied_string, 0); + if(non_init) { - if (vnm_exists) { - if (mu_index == MEAN_CURVATURE) - PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius).vertex_normal_map(vnm)); - else - PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius).vertex_normal_map(vnm)); - } - else { - if (mu_index == MEAN_CURVATURE) - PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); + if(vnm_exists) + { + if(mu_index == MEAN_CURVATURE) + { + PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, + CGAL::parameters::ball_radius(expand_radius) + .vertex_normal_map(vnm)); + } else - PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); + { + PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, + CGAL::parameters::ball_radius(expand_radius) + .vertex_normal_map(vnm)); + } } - double res_min = ARBITRARY_DBL_MAX, - res_max = -ARBITRARY_DBL_MAX; - SMesh::Vertex_index min_index, max_index; - for (SMesh::Vertex_index v : vertices(smesh)) + else { - if (mu_i_map[v] > res_max) + if(mu_index == MEAN_CURVATURE) { - res_max = mu_i_map[v]; - max_index = v; + PMP::interpolated_corrected_mean_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); } - if (mu_i_map[v] < res_min) + else { - res_min = mu_i_map[v]; - min_index = v; + PMP::interpolated_corrected_Gaussian_curvature(smesh, mu_i_map, CGAL::parameters::ball_radius(expand_radius)); } } -// if (mu_index == MEAN_CURVATURE){ -// mean_curvature_max[item] = std::make_pair(res_max, max_index); -// mean_curvature_min[item] = std::make_pair(res_min, min_index); -// } -// else { -// gaussian_curvature_max[item] = std::make_pair(res_max, max_index); -// gaussian_curvature_min[item] = std::make_pair(res_min, min_index); -// } } -// treat_sm_property(tied_string, item->face_graph()); + + displaySMProperty(tied_string, smesh); } private: @@ -1119,6 +1110,10 @@ private Q_SLOTS: zoomToSimplexWithPropertyExtremum(faces(mesh), mesh, "f:display_plugin_scaled_jacobian", extremum); else if(property_name == "Face Area") zoomToSimplexWithPropertyExtremum(faces(mesh), mesh, "f:display_plugin_area", extremum); + else if(property_name == "Interpolated Corrected Mean Curvature") + zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_interpolated_corrected_mean_curvature", extremum); + else if(property_name == "Interpolated Corrected Gaussian Curvature") + zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_interpolated_corrected_Gaussian_curvature", extremum); else if(property_simplex_types.at(property_index) == Property_simplex_type::VERTEX) zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, property_name, extremum); else if(property_simplex_types.at(property_index) == Property_simplex_type::FACE) @@ -1401,7 +1396,7 @@ scaled_jacobian(const face_descriptor f, for(std::size_t i=0; i Date: Wed, 2 Aug 2023 13:26:02 +0200 Subject: [PATCH 034/124] Fix UI + fix connections --- .../Plugins/Display/Display_property.ui | 316 ++++++++++-------- .../Display/Display_property_plugin.cpp | 27 +- 2 files changed, 189 insertions(+), 154 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui index d1e5e795e711..0a824308300d 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui @@ -3,7 +3,7 @@ DisplayPropertyWidget - false + true @@ -39,9 +39,18 @@ + + true + Property + + false + + + false + 6 @@ -82,13 +91,114 @@ + + + + true + + + 0 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksAbove + + + + + + + true + + + Expanding Radius: 0 + + + + + + + + + + false + + + Extreme Values + + + + + + Min Value + + + + + + + Max Value + + + + + + + 2.00 + + + true + + + + + + + Zoom to max value + + + + + + + Zoom to min value + + + + + + + 0.00 + + + true + + + true + + + - - - + + + Qt::Vertical @@ -100,7 +210,33 @@ - + + + + true + + + + + 0 + 0 + 236 + 335 + + + + + + + RAMP DISPLAYING + + + + + + + + Color Visualization @@ -118,10 +254,17 @@ 0 - - + + - Random colors + + + + + + + + Max color @@ -135,20 +278,6 @@ - - - - First color - - - - - - - Max color - - - @@ -159,58 +288,42 @@ - + - - + + - + Min color - - + + - Min color + First color + + + + + + + Random colors - - - - 0 - - - 100 - - - true - - - Qt::Horizontal - - - QSlider::TicksAbove - - - - - - - Expanding Radius: 0 - - - - - + + + + + Qt::Vertical @@ -222,97 +335,8 @@ - - - - true - - - - - 0 - 0 - 236 - 397 - - - - - - - RAMP DISPLAYING - - - - - - - - - - - false - - - Extreme Values - - - - - - Min Value - - - - - - - Max Value - - - - - - - 2.00 - - - true - - - - - - - Zoom to max value - - - - - - - Zoom to min value - - - - - - - 0.00 - - - true - - - true - - - - - - diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp index 037d5bb4982b..33a48a19e0ef 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp @@ -89,8 +89,8 @@ class Display_property_plugin double gI = 1.; double bI = 0.; - double expand_radius = 0; - double maxEdgeLength = -1; + double expand_radius = 0.; + double maxEdgeLength = -1.; Color_ramp color_ramp; std::vector color_map; @@ -209,8 +209,8 @@ class Display_property_plugin connect(dock_widget->zoomToMaxButton, &QPushButton::pressed, this, &Display_property_plugin::on_zoomToMaxButton_pressed); - connect(dock_widget->expandingRadiusSlider, SIGNAL(valueChanged(int)), - this, SLOT(setExpandingRadius(int))); + connect(dock_widget->expandingRadiusSlider, &QSlider::valueChanged, + this, &Display_property_plugin::setExpandingRadius); } private Q_SLOTS: @@ -512,6 +512,15 @@ private Q_SLOTS: { dock_widget->setEnabled(true); disableExtremeValues(); // only available after coloring + + // Curvature property-specific slider + const std::string& property_name = dock_widget->propertyBox->currentText().toStdString(); + const bool is_curvature_property = (property_name == "Interpolated Corrected Mean Curvature" || + property_name == "Interpolated Corrected Gaussian Curvature"); + dock_widget->expandingRadiusLabel->setVisible(is_curvature_property); + dock_widget->expandingRadiusSlider->setVisible(is_curvature_property); + dock_widget->expandingRadiusLabel->setEnabled(is_curvature_property); + dock_widget->expandingRadiusSlider->setEnabled(is_curvature_property); } else // no or broken property { @@ -854,11 +863,12 @@ private Q_SLOTS: displaySMProperty("f:display_plugin_area", *sm); } - void setExpandingRadius(const int val_int) +private Q_SLOTS: + void setExpandingRadius() { double sliderMin = dock_widget->expandingRadiusSlider->minimum(); double sliderMax = dock_widget->expandingRadiusSlider->maximum() - sliderMin; - double val = val_int - sliderMin; + double val = dock_widget->expandingRadiusSlider->value() - sliderMin; sliderMin = 0; Scene_item* item = scene->item(scene->mainSelectionIndex()); @@ -878,7 +888,7 @@ private Q_SLOTS: if(num_edges(smesh) == 0) { expand_radius = 0; - dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius: %1").arg(expand_radius)); return; } @@ -901,9 +911,10 @@ private Q_SLOTS: double outMax = 5 * maxEdgeLength, base = 1.2; expand_radius = (pow(base, val) - 1) * outMax / (pow(base, sliderMax) - 1); - dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius : %1").arg(expand_radius)); + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius: %1").arg(expand_radius)); } +private: void displayInterpolatedCurvatureMeasure(Scene_surface_mesh_item* item, CurvatureType mu_index) { From 15ad1f78eb521f66620f7f99792f4581ccf16db1 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 4 Aug 2023 11:20:52 +0200 Subject: [PATCH 035/124] Change example input to be analogous to uniform sizing --- .../isotropic_remeshing_with_sizing_example.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index e0564d27178d..cb4cdef20ac8 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -15,10 +16,9 @@ int main(int argc, char* argv[]) // const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/hand.off"); const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/nefertiti.off"); // const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube.off"); - std::ifstream input(filename); Mesh mesh; - if (!input || !(input >> mesh) || !CGAL::is_triangle_mesh(mesh)) { + if (!PMP::IO::read_polygon_mesh(filename, mesh) || !CGAL::is_triangle_mesh(mesh)) { std::cerr << "Not a valid input file." << std::endl; return 1; } From 1f2c0f2471e02ca8a20894927a4c480141adedc7 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 4 Aug 2023 13:15:32 +0200 Subject: [PATCH 036/124] Remove extra --- .../Adaptive_sizing_field.h | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index dc14ae91d119..dea4dc457282 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -149,29 +149,6 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return get(m_vertex_sizing_map, v); } - template - void calc_sizing_map(const FaceRange& face_range) - { - if (face_range.size() == faces(m_pmesh).size()) - { - // calculate curvature from the whole mesh - calc_sizing_map(m_pmesh); - } - else - { - // expand face selection and calculate curvature from it - std::vector selection(face_range.begin(), face_range.end()); - auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); - for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); - for (face_descriptor f : face_range) put(is_selected, f, true); - CGAL::expand_face_selection(selection, m_pmesh, 1 - , is_selected, std::back_inserter(selection)); - CGAL::Face_filtered_graph ffg(m_pmesh, selection); - - calc_sizing_map(ffg); - } - } - boost::optional is_too_long(const halfedge_descriptor h) const { const FT sqlen = sqlength(h); From 00b4b93d1cf613f91dfe9c4c8f815b08b86d1d1f Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sun, 6 Aug 2023 12:16:31 +0200 Subject: [PATCH 037/124] Remove the adaptive sizing field dependency in remesh.h --- .../examples/Polygon_mesh_processing/CMakeLists.txt | 8 ++------ .../isotropic_remeshing_with_sizing_example.cpp | 1 + .../internal/Isotropic_remeshing/Adaptive_sizing_field.h | 1 + .../include/CGAL/Polygon_mesh_processing/remesh.h | 1 - .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 3 +++ 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt index 3644cc490978..590305c0c8ad 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/CMakeLists.txt @@ -25,8 +25,8 @@ 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("isotropic_remeshing_example.cpp") -#create_single_source_cgal_program("isotropic_remeshing_of_patch_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("tangential_relaxation_example.cpp") create_single_source_cgal_program("surface_mesh_intersection.cpp") create_single_source_cgal_program("corefinement_SM.cpp") @@ -69,10 +69,6 @@ if(TARGET CGAL::Eigen3_support) target_link_libraries(hole_filling_example_LCC PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("mesh_smoothing_example.cpp") target_link_libraries(mesh_smoothing_example PUBLIC CGAL::Eigen3_support) - create_single_source_cgal_program("isotropic_remeshing_example.cpp") - target_link_libraries(isotropic_remeshing_example PUBLIC CGAL::Eigen3_support) - create_single_source_cgal_program("isotropic_remeshing_of_patch_example.cpp") - target_link_libraries(isotropic_remeshing_of_patch_example PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("isotropic_remeshing_with_sizing_example.cpp") target_link_libraries(isotropic_remeshing_with_sizing_example PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("delaunay_remeshing_example.cpp") diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index cb4cdef20ac8..5d2ad63792ec 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h index dea4dc457282..2b56d6691392 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h @@ -79,6 +79,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field template void calc_sizing_map(FaceGraph& face_graph) { + //todo ip: please check if this is good enough to store curvature typedef Principal_curvatures_and_directions Principal_curvatures; typedef typename CGAL::dynamic_vertex_property_t Vertex_curvature_tag; typedef typename boost::property_map #include -#include #include #include diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 52076c1cc4c6..e495afc7e6e9 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -405,6 +406,7 @@ public Q_SLOTS: else //not edges_only { if(protect && + edge_sizing_type == 0 && //todo ip: current solution for adaptive remeshing !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( *selection_item->polyhedron(), selection_item->constrained_edges_pmap(), @@ -672,6 +674,7 @@ public Q_SLOTS: } if(protect && + edge_sizing_type == 0 && //todo ip: current solution for adaptive remeshing !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( pmesh, ecm, From 64257b9c665fb538ca7bc66aef47de5413335d6e Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 7 Aug 2023 09:11:49 +0200 Subject: [PATCH 038/124] Add remeshing quality test --- .../Polygon_mesh_processing/CMakeLists.txt | 6 +- .../remeshing_quality_test.cpp | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt index 8ba77a7ff15e..6ec0f6e00139 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/CMakeLists.txt @@ -80,10 +80,12 @@ if(TARGET CGAL::Eigen3_support) target_link_libraries(test_shape_smoothing PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("delaunay_remeshing_test.cpp") target_link_libraries(delaunay_remeshing_test PUBLIC CGAL::Eigen3_support) - create_single_source_cgal_program("test_interpolated_corrected_curvatures.cpp") - target_link_libraries(test_interpolated_corrected_curvatures PUBLIC CGAL::Eigen3_support) create_single_source_cgal_program("test_decimation_of_planar_patches.cpp") target_link_libraries(test_decimation_of_planar_patches PUBLIC CGAL::Eigen3_support) + create_single_source_cgal_program("test_interpolated_corrected_curvatures.cpp") + target_link_libraries(test_interpolated_corrected_curvatures PUBLIC CGAL::Eigen3_support) + create_single_source_cgal_program("remeshing_quality_test.cpp" ) + target_link_libraries(remeshing_quality_test PUBLIC CGAL::Eigen3_support) else() message(STATUS "NOTICE: Tests that use the Eigen library will not be compiled.") endif() diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp new file mode 100644 index 000000000000..6f04ffb07fb9 --- /dev/null +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +#include + +typedef CGAL::Exact_predicates_inexact_constructions_kernel K; +typedef K::Point_3 Point_3; +typedef CGAL::Surface_mesh Mesh; + +namespace PMP = CGAL::Polygon_mesh_processing; + +int main(int argc, char* argv[]) +{ + const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/hand.off"); + + Mesh mesh; + if (!PMP::IO::read_polygon_mesh(filename, mesh) || !CGAL::is_triangle_mesh(mesh)) + { + std::cerr << "Not a valid input file." << std::endl; + return 1; + } + + std::cout << "Start remeshing of " << filename + << " (" << num_faces(mesh) << " faces)..." << std::endl; + + const double tol = 0.001; + const std::pair edge_min_max{0.001, 0.5}; + PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, faces(mesh), mesh); + const unsigned int nb_iter = 5; + + PMP::isotropic_remeshing( + faces(mesh), + sizing_field, + mesh, + PMP::parameters::number_of_iterations(nb_iter).number_of_relaxation_steps(3) + ); + + std::cout << "Remeshing done, checking triangle quality...\n" << std::endl; + double qmin = std::numeric_limits::max(); // minimum triangle quality + double qavg = 0.; // average quality + double min_angle = std::numeric_limits::max(); // minimum angle + double avg_min_angle = 0.; // average minimum angle + for (auto face : mesh.faces()) + { + if (PMP::is_degenerate_triangle_face(face, mesh)) + { + std::cout << "Found degenerate triangle!" << std::endl; + continue; //todo ip should something be done about this? + } + + // Calculate Q(t) triangle quality indicator + std::vector pts; pts.reserve(3); + for (auto vx : mesh.vertices_around_face(mesh.halfedge(face))) + pts.push_back(mesh.point(vx)); + + double half_perim = 0.; // half-perimeter + double lmax = 0.; // longest edge + for (int i = 0; i < 3; ++i) + { + const double length = CGAL::sqrt(CGAL::squared_distance(pts[i], pts[(i + 1) % 2])); + + half_perim += length; + if (length > lmax) lmax = length; + } + half_perim /= 2.; + const double area = CGAL::sqrt(CGAL::squared_area(pts[0], pts[1], pts[2])); + + const double face_quality = 6. / CGAL::sqrt(3.) * area / half_perim / lmax; + + qavg += face_quality; + if (face_quality < qmin) qmin = face_quality; + + // Calculate minimum angle + const auto v0 = pts[1] - pts[0]; + const auto v1 = pts[2] - pts[0]; + const auto v2 = pts[2] - pts[1]; + + const double dotp0 = CGAL::scalar_product(v0,v1); + const double angle0 = acos(dotp0 / (sqrt(v0.squared_length()) * sqrt(v1.squared_length()))); + const double dotp1 = CGAL::scalar_product(-v0, v2); + const double angle1 = acos(dotp1 / (sqrt(v0.squared_length()) * sqrt(v2.squared_length()))); + const double angle2 = CGAL_PI - (angle0 + angle1); + + double curr_min_angle = angle1; + if (angle1 < curr_min_angle) curr_min_angle = angle1; + if (angle2 < curr_min_angle) curr_min_angle = angle2; + + avg_min_angle += curr_min_angle; + if (curr_min_angle < min_angle) min_angle = curr_min_angle; + } + qavg /= mesh.number_of_faces(); + avg_min_angle /= mesh.number_of_faces(); + + std::cout << "Mesh size: " << mesh.number_of_faces() << std::endl; + std::cout << "Average quality: " << qavg << std::endl; + std::cout << "Minimum quality: " << qmin << std::endl; + std::cout << "Average minimum angle: " << avg_min_angle * 180. / CGAL_PI + << " deg" << std::endl; + std::cout << "Minimum angle: " << min_angle * 180. / CGAL_PI + << " deg" << std::endl; + + return 0; +} \ No newline at end of file From 36f8d39f92040b70d5f1d7d2f60d9c44ec4b81d4 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 7 Aug 2023 20:59:06 +0200 Subject: [PATCH 039/124] Remove extra code in tangential smoothing --- .../tangential_relaxation.h | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index d2e955841f11..c0103f943299 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -363,12 +363,6 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, typedef typename GT::Vector_3 Vector_3; typedef typename GT::Point_3 Point_3; - //todo ip: alt calc - typename GT::Construct_vector_3 vector = gt.construct_vector_3_object(); - typename GT::Compute_scalar_product_3 scalar_product = gt.compute_scalar_product_3_object(); - typename GT::Compute_squared_length_3 squared_length = gt.compute_squared_length_3_object(); - typename GT::Construct_cross_product_vector_3 cross_product = gt.construct_cross_product_vector_3_object(); - auto check_normals = [&](vertex_descriptor v) { bool first_run = true; @@ -450,28 +444,18 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, interior_hedges.push_back(h); } - //todo ip: handle border edges with sizing field if (border_halfedges.empty()) { const Vector_3& vn = vnormals.at(v); Vector_3 move = CGAL::NULL_VECTOR; double weight = 0; -// unsigned int star_size = 0; for(halfedge_descriptor h :interior_hedges) { // calculate weight - // need v0, v1 and v2 + // need v, v1 and v2 const vertex_descriptor v1 = target(next(h, tm), tm); const vertex_descriptor v2 = source(h, tm); - //todo ip- alt calc -// const Vector_3 vec0 = vector(get(vpm, v), get(vpm, v1)); -// const Vector_3 vec1 = vector(get(vpm, v), get(vpm, v2)); -// const double sqarea = squared_length(cross_product(vec0, vec1)); -// const double face_weight = CGAL::approximate_sqrt(sqarea) -// / pow(1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2)), 2); - - //todo ip- paper implementation const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); const double face_weight = tri_area / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); @@ -484,7 +468,7 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, barycenters.emplace_back(v, vn, get(vpm, v) + move); } - else + else //todo ip: do border edges need to be handled with the sizing field? { if (!relax_constraints) continue; Vector_3 vn(NULL_VECTOR); From 4a8974d256ccd06cc275990099cba8aba9d899ad Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 8 Aug 2023 10:17:52 +0200 Subject: [PATCH 040/124] Move sizing classes to 'public' headers --- .../isotropic_remeshing_with_sizing_example.cpp | 2 +- .../Isotropic_remeshing => }/Adaptive_sizing_field.h | 0 .../Isotropic_remeshing => }/Uniform_sizing_field.h | 0 .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 5 ++++- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 2 +- 6 files changed, 7 insertions(+), 4 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{internal/Isotropic_remeshing => }/Adaptive_sizing_field.h (100%) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{internal/Isotropic_remeshing => }/Uniform_sizing_field.h (100%) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 5d2ad63792ec..dacc3e02e1b2 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h similarity index 100% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Adaptive_sizing_field.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h similarity index 100% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Uniform_sizing_field.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h 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 dc11e0d52a4a..ac815afa51ab 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index 6f04ffb07fb9..01f97b210cb8 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include @@ -38,6 +38,9 @@ int main(int argc, char* argv[]) PMP::parameters::number_of_iterations(nb_iter).number_of_relaxation_steps(3) ); + /* + * More information on quality metrics can be found here: https://ieeexplore.ieee.org/document/9167456 + */ std::cout << "Remeshing done, checking triangle quality...\n" << std::endl; double qmin = std::numeric_limits::max(); // minimum triangle quality double qavg = 0.; // average quality diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index e495afc7e6e9..a63e88a69031 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include #include #include From 050c7f951235904b5a8251860f26000b18b68406 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 10 Aug 2023 21:33:27 +0200 Subject: [PATCH 041/124] Add split_long_edges functionality using adaptive sizing field as an input --- .../Isotropic_remeshing/remesh_impl.h | 36 ++++--- .../CGAL/Polygon_mesh_processing/remesh.h | 26 ++++- .../PMP/Isotropic_remeshing_plugin.cpp | 101 +++++++++++++++--- 3 files changed, 130 insertions(+), 33 deletions(-) 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 80ab1f342a0c..bb65af3e2d10 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 @@ -383,19 +383,17 @@ namespace internal { } } - // split edges of edge_range that have their length > high // Note: only used to split a range of edges provided as input - template + template void split_long_edges(const EdgeRange& edge_range, - const double& high) + SizingFunction& sizing) { #ifdef CGAL_PMP_REMESHING_VERBOSE - std::cout << "Split long edges (" << high << ")..."; - std::cout.flush(); +// std::cout << "Split long edges (" << high << ")..."; +// std::cout.flush(); #endif - double sq_high = high*high; //collect long edges typedef std::pair H_and_sql; @@ -406,9 +404,9 @@ namespace internal { ); for(edge_descriptor e : edge_range) { - double sqlen = sqlength(e); - if (sqlen > sq_high) - long_edges.emplace(halfedge(e, mesh_), sqlen); + boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_)); + if(sqlen != boost::none) + long_edges.emplace(halfedge(e, mesh_), sqlen.get()); } //split long edges @@ -439,15 +437,19 @@ namespace internal { #ifdef CGAL_PMP_REMESHING_VERY_VERBOSE std::cout << " refinement point : " << refinement_point << std::endl; #endif + //update sizing field with the new point + if constexpr (!std::is_same_v>) + sizing.update_sizing_map(vnew); //check sub-edges - double sqlen_new = 0.25 * sqlen; - if (sqlen_new > sq_high) - { - //if it was more than twice the "long" threshold, insert them - long_edges.emplace(hnew, sqlen_new); - long_edges.emplace(next(hnew, mesh_), sqlen_new); - } + //if it was more than twice the "long" threshold, insert them + boost::optional sqlen_new = sizing.is_too_long(hnew); + if(sqlen_new != boost::none) + long_edges.emplace(hnew, sqlen_new.get()); + + sqlen_new = sizing.is_too_long(next(hnew, mesh_)); + if (sqlen_new != boost::none) + long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); //insert new edges to keep triangular faces, and update long_edges if (!is_border(hnew, mesh_)) @@ -552,7 +554,7 @@ namespace internal { halfedge_added(hnew, status(he)); halfedge_added(hnew_opp, status(opposite(he, mesh_))); - //todo ip-add: already updating sizing here because of is_too_long checks below + //update sizing field with the new point if constexpr (!std::is_same_v>) sizing.update_sizing_map(vnew); 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 ac815afa51ab..7d88ad209f69 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -424,9 +424,10 @@ void isotropic_remeshing(const FaceRange& faces */ template void split_long_edges(const EdgeRange& edges - , const double& max_length + , SizingFunction& sizing , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { @@ -473,7 +474,28 @@ void split_long_edges(const EdgeRange& edges fimap, false/*need aabb_tree*/); - remesher.split_long_edges(edges, max_length); + // check if sizing field needs updating + if constexpr (!std::is_same_v>) + { + //todo ip: check if sizing field needs to be checked and updated here + } + + remesher.split_long_edges(edges, sizing); +} + +//todo ip: documentation +// overload when using max_length +template +void split_long_edges(const EdgeRange& edges + , const double max_length + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) +{ + // construct the uniform field, 3/4 as m_sq_long is stored with 4/3 of length + Uniform_sizing_field sizing(3. / 4. * max_length, pmesh); + split_long_edges(edges, sizing, pmesh, np); } } //end namespace Polygon_mesh_processing diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index a63e88a69031..00e34c5e63eb 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -294,9 +294,13 @@ class Polyhedron_demo_isotropic_remeshing_plugin : PMP::triangulate_face(f_and_p.first, *f_and_p.second); } - void do_split_edges(Scene_polyhedron_selection_item* selection_item, + void do_split_edges(const int edge_sizing_type, + Scene_polyhedron_selection_item* selection_item, SMesh& pmesh, - double target_length) + const double target_length, + const double error_tol, + const double min_length, + const double max_length) { std::vector p_edges; for(edge_descriptor e : edges(pmesh)) @@ -314,12 +318,32 @@ class Polyhedron_demo_isotropic_remeshing_plugin : } } if (!p_edges.empty()) - CGAL::Polygon_mesh_processing::split_long_edges( - p_edges - , target_length - , *selection_item->polyhedron() - , CGAL::parameters::geom_traits(EPICK()) + { + if (edge_sizing_type == 0) + { + CGAL::Polygon_mesh_processing::split_long_edges( + p_edges + , target_length + , *selection_item->polyhedron() + , CGAL::parameters::geom_traits(EPICK()) + .edge_is_constrained_map(selection_item->constrained_edges_pmap())); + } + else if (edge_sizing_type == 1) + { + std::pair edge_min_max{min_length, max_length}; + CGAL::Polygon_mesh_processing::Adaptive_sizing_field adaptive_sizing( + error_tol + , edge_min_max + , faces(*selection_item->polyhedron()) + , *selection_item->polyhedron()); + CGAL::Polygon_mesh_processing::split_long_edges( + p_edges + , adaptive_sizing + , *selection_item->polyhedron() + , CGAL::parameters::geom_traits(EPICK()) .edge_is_constrained_map(selection_item->constrained_edges_pmap())); + } + } else std::cout << "No selected or boundary edges to be split" << std::endl; } @@ -401,7 +425,7 @@ public Q_SLOTS: { if (edges_only) { - do_split_edges(selection_item, pmesh, target_length); + do_split_edges(edge_sizing_type, selection_item, pmesh, target_length, error_tol, min_length, max_length); } else //not edges_only { @@ -438,7 +462,7 @@ public Q_SLOTS: } else { - do_split_edges(selection_item, pmesh, target_length); + do_split_edges(edge_sizing_type, selection_item, pmesh, target_length, error_tol, min_length, max_length); } } @@ -637,6 +661,9 @@ public Q_SLOTS: if (!edges_to_split.empty()) { if (fpmap_valid) + { + if (edge_sizing_type == 0) + { CGAL::Polygon_mesh_processing::split_long_edges( edges_to_split , target_length @@ -644,13 +671,49 @@ public Q_SLOTS: , CGAL::parameters::geom_traits(EPICK()) . edge_is_constrained_map(eif) . face_patch_map(fpmap)); + } + else if (edge_sizing_type == 1) + { + std::pair edge_min_max{min_length, max_length}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + , edge_min_max + , faces(pmesh) + , pmesh); + CGAL::Polygon_mesh_processing::split_long_edges( + edges_to_split + , target_length + , pmesh + , CGAL::parameters::geom_traits(EPICK()) + . edge_is_constrained_map(eif) + . face_patch_map(fpmap)); + } + } else + { + if (edge_sizing_type == 0) + { CGAL::Polygon_mesh_processing::split_long_edges( edges_to_split , target_length , pmesh , CGAL::parameters::geom_traits(EPICK()) . edge_is_constrained_map(eif)); + } + else if (edge_sizing_type = 1) + { + std::pair edge_min_max{min_length, max_length}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol + , edge_min_max + , faces(pmesh) + , pmesh); + CGAL::Polygon_mesh_processing::split_long_edges( + edges_to_split + , adaptive_sizing_field + , pmesh + , CGAL::parameters::geom_traits(EPICK()) + . edge_is_constrained_map(eif)); + } + } } else std::cout << "No border to be split" << std::endl; @@ -968,10 +1031,25 @@ public Q_SLOTS: for(halfedge_descriptor h : border) border_edges.push_back(edge(h, *poly_item->polyhedron())); + if (edge_sizing_type_ == 0) + { CGAL::Polygon_mesh_processing::split_long_edges( border_edges , target_length_ , *poly_item->polyhedron()); + } + else if (edge_sizing_type_ == 1) + { + std::pair edge_min_max{min_length_, max_length_}; + PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ + , edge_min_max + , faces(*poly_item->polyhedron()) + , *poly_item->polyhedron()); + CGAL::Polygon_mesh_processing::split_long_edges( + border_edges + , target_length_ + , *poly_item->polyhedron()); + } } else { @@ -1130,9 +1208,6 @@ public Q_SLOTS: ui.smooth1D_label->setEnabled(false); ui.smooth1D_checkbox->setEnabled(false); ui.smooth1D_checkbox->setChecked(false); - - ui.edgeSizing_type_combo_box->setCurrentIndex(0); - ui.edgeSizing_type_combo_box->setEnabled(false); } else { @@ -1146,8 +1221,6 @@ public Q_SLOTS: ui.smooth1D_label->setEnabled(true); ui.smooth1D_checkbox->setEnabled(true); - - ui.edgeSizing_type_combo_box->setEnabled(true); } } From 600f72fd0ecb521eeafba5f897e0b94548ebe72d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 11 Aug 2023 15:09:23 +0200 Subject: [PATCH 042/124] Reintroduce constraints_are_short_enough for adaptive remeshing --- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 00e34c5e63eb..940d576735e4 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -430,7 +430,6 @@ public Q_SLOTS: else //not edges_only { if(protect && - edge_sizing_type == 0 && //todo ip: current solution for adaptive remeshing !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( *selection_item->polyhedron(), selection_item->constrained_edges_pmap(), @@ -737,7 +736,6 @@ public Q_SLOTS: } if(protect && - edge_sizing_type == 0 && //todo ip: current solution for adaptive remeshing !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( pmesh, ecm, @@ -1313,7 +1311,7 @@ public Q_SLOTS: ui.nbIterations_spinbox->setValue(1); ui.edgeSizing_type_combo_box->setCurrentIndex(0); - on_edgeSizing_type_combo_box_changed(0); //todo ip otherwise it shows all remeshing variables + on_edgeSizing_type_combo_box_changed(0); ui.protect_checkbox->setChecked(false); ui.smooth1D_checkbox->setChecked(true); From f589b054ed89d59e3554a416728d8ead6ab3f837 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 11 Aug 2023 15:46:21 +0200 Subject: [PATCH 043/124] Documentation update in remesh.h --- .../CGAL/Polygon_mesh_processing/remesh.h | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) 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 7d88ad209f69..e22b48013068 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -31,16 +31,6 @@ namespace CGAL { namespace Polygon_mesh_processing { -/*! \todo document me or merge the doc with the original overload*/ -template -void isotropic_remeshing(const FaceRange& faces - , SizingFunction& sizing - , PolygonMesh& pmesh - , const NamedParameters& np); - /*! * \ingroup PMP_meshing_grp * @@ -54,12 +44,14 @@ void isotropic_remeshing(const FaceRange& faces * and `boost::graph_traits::%halfedge_descriptor` must be * models of `Hashable`. * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, - model of `Range`. Its iterator type is `ForwardIterator`. +* model of `Range`. Its iterator type is `ForwardIterator`. +* @tparam SizingFunction model of `Sizing_field` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param pmesh a polygon mesh with triangulated surface patches to be remeshed * @param faces the range of triangular faces defining one or several surface patches to be remeshed -* @param target_edge_length the edge length that is targeted in the remeshed patch. +* @param sizing uniform or adaptive sizing field that determines a target length for individual edges. +* If a number is passed, it uses uniform sizing with the number as a target edge length. * If `0` is passed then only the edge-flip, tangential relaxation, and projection steps will be done. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * @@ -202,23 +194,6 @@ void isotropic_remeshing(const FaceRange& faces * get a point which is exactly on the surface. * */ -template -void isotropic_remeshing(const FaceRange& faces - , const double target_edge_length - , PolygonMesh& pmesh - , const NamedParameters& np = parameters::default_values()) -{ - typedef Uniform_sizing_field Default_sizing; - Default_sizing sizing(target_edge_length, pmesh); - isotropic_remeshing( - faces, - sizing, - pmesh, - np); -} - template +void isotropic_remeshing(const FaceRange& faces + , const double target_edge_length + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) +{ + typedef Uniform_sizing_field Default_sizing; + Default_sizing sizing(target_edge_length, pmesh); + isotropic_remeshing( + faces, + sizing, + pmesh, + np); +} + /*! * \ingroup PMP_meshing_grp * @brief splits the edges listed in `edges` into sub-edges @@ -374,12 +367,13 @@ void isotropic_remeshing(const FaceRange& faces * has an internal property map for `CGAL::vertex_point_t`. * @tparam EdgeRange range of `boost::graph_traits::%edge_descriptor`, * model of `Range`. Its iterator type is `InputIterator`. +* @tparam SizingFunction model of `Sizing_field` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param pmesh a polygon mesh * @param edges the range of edges to be split if they are longer than given threshold -* @param max_length the edge length above which an edge from `edges` is split -* into to sub-edges +* @param sizing the sizing function that is used to split edges from 'edges' list. If a number is passed, +* all edges longer than the number are split into sub-edges. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin @@ -483,8 +477,7 @@ void split_long_edges(const EdgeRange& edges remesher.split_long_edges(edges, sizing); } -//todo ip: documentation -// overload when using max_length +// Convenience overload when using max_length for sizing template From 66721bbcd9ea9a4edd6423d2974b403a358a93c7 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sat, 12 Aug 2023 17:36:16 +0200 Subject: [PATCH 044/124] Add precondition 'remeshing mesh == sizing field mesh' --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 ++ .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 1 + .../internal/Isotropic_remeshing/Sizing_field.h | 1 + .../include/CGAL/Polygon_mesh_processing/remesh.h | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 2b56d6691392..543d6645844a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -213,6 +213,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field put(m_vertex_sizing_map, v, vertex_size); } + const PolygonMesh& get_mesh() const { return m_pmesh; } + //todo ip: is_protected_constraint_too_long() from PR private: diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index a36c30aa712c..1cc96e883444 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -92,6 +92,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field get(vpmap, source(h, m_pmesh))); } + const PolygonMesh& get_mesh() const { return m_pmesh; } private: FT m_sq_short; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index 3640e14c092d..b60148c3e73d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -45,6 +45,7 @@ class Sizing_field const vertex_descriptor vb) const = 0; virtual boost::optional is_too_short(const halfedge_descriptor h) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h) const = 0; + virtual const PolygonMesh& get_mesh() const = 0; }; 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 e22b48013068..91a9ed2d6074 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -206,6 +206,10 @@ void isotropic_remeshing(const FaceRange& faces if (std::begin(faces)==std::end(faces)) return; + //todo ip: precondition or something else? + CGAL_precondition_msg(&(sizing.get_mesh()) == &pmesh, "Input mesh is not the same " + "as the one used for the sizing field!"); + typedef PolygonMesh PM; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename boost::graph_traits::edge_descriptor edge_descriptor; From ee640c91dd6817ca8afc05086e4d4f282693f450 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sun, 13 Aug 2023 10:41:16 +0200 Subject: [PATCH 045/124] Handle the special case when target_edge_length is 0 --- .../CGAL/Polygon_mesh_processing/remesh.h | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) 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 91a9ed2d6074..d146fb57597e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -339,22 +339,20 @@ void isotropic_remeshing(const FaceRange& faces #endif } -// Convenience overload when using target_edge_length for sizing +// Overload when using target_edge_length for sizing template + , typename FaceRange + , typename NamedParameters = parameters::Default_named_parameters> void isotropic_remeshing(const FaceRange& faces - , const double target_edge_length - , PolygonMesh& pmesh - , const NamedParameters& np = parameters::default_values()) + , const double target_edge_length + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) { - typedef Uniform_sizing_field Default_sizing; - Default_sizing sizing(target_edge_length, pmesh); - isotropic_remeshing( - faces, - sizing, - pmesh, - np); + Uniform_sizing_field sizing(target_edge_length, pmesh); + if (target_edge_length > 0) + isotropic_remeshing(faces, sizing, pmesh, np); + else + isotropic_remeshing(faces, sizing, pmesh, np.do_split(false).do_collapse(false)); } /*! From 6ee23c6fdddabbb95bdf1109b4f571f34f70966f Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 15 Aug 2023 20:07:58 +0200 Subject: [PATCH 046/124] Replace pmesh with vertex property maps in sizing field classes --- .../Adaptive_sizing_field.h | 78 +++++++++---------- .../Uniform_sizing_field.h | 45 +++++------ .../Isotropic_remeshing/Sizing_field.h | 16 ++-- .../Isotropic_remeshing/remesh_impl.h | 36 ++++----- .../CGAL/Polygon_mesh_processing/remesh.h | 8 +- .../PMP/Isotropic_remeshing_plugin.cpp | 2 +- 6 files changed, 89 insertions(+), 96 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 543d6645844a..1e8151c14b32 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -38,10 +38,11 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename Base::face_descriptor face_descriptor; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; + typedef typename Base::DefaultVPMap DefaultVPMap; typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map::type Vertex_sizing_map; + Vertex_property_tag>::type VertexSizingMap; template Adaptive_sizing_field(const double tol @@ -51,25 +52,24 @@ class Adaptive_sizing_field : public CGAL::Sizing_field : tol(tol) , m_short(edge_len_min_max.first) , m_long(edge_len_min_max.second) - , m_pmesh(pmesh) + , m_vpmap(get(CGAL::vertex_point, pmesh)) + , m_vertex_sizing_map(get(Vertex_property_tag(), pmesh)) { - m_vertex_sizing_map = get(Vertex_property_tag(), m_pmesh); - - if (face_range.size() == faces(m_pmesh).size()) + if (face_range.size() == faces(pmesh).size()) { // calculate curvature from the whole mesh - calc_sizing_map(m_pmesh); + calc_sizing_map(pmesh); } else { // expand face selection and calculate curvature from it std::vector selection(face_range.begin(), face_range.end()); - auto is_selected = get(CGAL::dynamic_face_property_t(), m_pmesh); - for (face_descriptor f : faces(m_pmesh)) put(is_selected, f, false); + auto is_selected = get(CGAL::dynamic_face_property_t(), pmesh); + for (face_descriptor f : faces(pmesh)) put(is_selected, f, false); for (face_descriptor f : face_range) put(is_selected, f, true); - CGAL::expand_face_selection(selection, m_pmesh, 1 + CGAL::expand_face_selection(selection, pmesh, 1 , is_selected, std::back_inserter(selection)); - CGAL::Face_filtered_graph ffg(m_pmesh, selection); + CGAL::Face_filtered_graph ffg(pmesh, selection); calc_sizing_map(ffg); } @@ -134,14 +134,12 @@ class Adaptive_sizing_field : public CGAL::Sizing_field FT sqlength(const vertex_descriptor va, const vertex_descriptor vb) const { - typename boost::property_map::const_type - vpmap = get(CGAL::vertex_point, m_pmesh); - return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); + return FT(CGAL::squared_distance(get(m_vpmap, va), get(m_vpmap, vb))); } - FT sqlength(const halfedge_descriptor& h) const + FT sqlength(const halfedge_descriptor& h, const PolygonMesh& pmesh) const { - return sqlength(target(h, m_pmesh), source(h, m_pmesh)); + return sqlength(target(h, pmesh), source(h, pmesh)); } public: @@ -150,13 +148,13 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return get(m_vertex_sizing_map, v); } - boost::optional is_too_long(const halfedge_descriptor h) const + boost::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - const FT sqlen = sqlength(h); - FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh)))); - CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); - CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); + const FT sqlen = sqlength(h, pmesh); + FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), + get(m_vertex_sizing_map, target(h, pmesh)))); + CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); if(sqlen > sqtarg_len) return sqlen; else @@ -177,52 +175,46 @@ class Adaptive_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_short(const halfedge_descriptor h) const + boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - const FT sqlen = sqlength(h); - FT sqtarg_len = CGAL::square(4./5. * CGAL::min(get(m_vertex_sizing_map, source(h, m_pmesh)), - get(m_vertex_sizing_map, target(h, m_pmesh)))); - CGAL_assertion(get(m_vertex_sizing_map, source(h, m_pmesh))); - CGAL_assertion(get(m_vertex_sizing_map, target(h, m_pmesh))); + const FT sqlen = sqlength(h, pmesh); + FT sqtarg_len = CGAL::square(4./5. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), + get(m_vertex_sizing_map, target(h, pmesh)))); + CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); + CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); if (sqlen < sqtarg_len) return sqlen; else return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor h) const + virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - typename boost::property_map::const_type - vpmap = get(CGAL::vertex_point, m_pmesh); - return CGAL::midpoint(get(vpmap, target(h, m_pmesh)), - get(vpmap, source(h, m_pmesh))); + return CGAL::midpoint(get(m_vpmap, target(h, pmesh)), + get(m_vpmap, source(h, pmesh))); } - void update_sizing_map(const vertex_descriptor v) + void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) { // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split FT vertex_size = 0; - CGAL_assertion(CGAL::halfedges_around_target(v, m_pmesh).size() == 2); - for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, m_pmesh)) + CGAL_assertion(CGAL::halfedges_around_target(v, pmesh).size() == 2); + for (halfedge_descriptor ha: CGAL::halfedges_around_target(v, pmesh)) { - vertex_size += get(m_vertex_sizing_map, source(ha, m_pmesh)); + vertex_size += get(m_vertex_sizing_map, source(ha, pmesh)); } - vertex_size /= CGAL::halfedges_around_target(v, m_pmesh).size(); + vertex_size /= CGAL::halfedges_around_target(v, pmesh).size(); put(m_vertex_sizing_map, v, vertex_size); } - const PolygonMesh& get_mesh() const { return m_pmesh; } - - //todo ip: is_protected_constraint_too_long() from PR - private: const FT tol; const FT m_short; const FT m_long; - PolygonMesh& m_pmesh; - Vertex_sizing_map m_vertex_sizing_map; + const DefaultVPMap m_vpmap; + VertexSizingMap m_vertex_sizing_map; }; }//end namespace Polygon_mesh_processing diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 1cc96e883444..2e047548adaa 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -23,11 +23,12 @@ namespace CGAL { namespace Polygon_mesh_processing { -template -class Uniform_sizing_field : public CGAL::Sizing_field +template ::const_type> +class Uniform_sizing_field : public CGAL::Sizing_field { private: - typedef CGAL::Sizing_field Base; + typedef CGAL::Sizing_field Base; public: typedef typename Base::FT FT; @@ -35,30 +36,34 @@ class Uniform_sizing_field : public CGAL::Sizing_field typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; - Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + Uniform_sizing_field(const FT size, const VPMap& vpmap) : m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) - , m_pmesh(pmesh) + , m_vpmap(vpmap) + {} + + Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + : m_sq_short( CGAL::square(4./5. * size)) + , m_sq_long( CGAL::square(4./3. * size)) + , m_vpmap(get(CGAL::vertex_point, pmesh)) {} private: FT sqlength(const vertex_descriptor va, const vertex_descriptor vb) const { - typename boost::property_map::const_type - vpmap = get(CGAL::vertex_point, m_pmesh); - return FT(CGAL::squared_distance(get(vpmap, va), get(vpmap, vb))); + return FT(CGAL::squared_distance(get(m_vpmap, va), get(m_vpmap, vb))); } - FT sqlength(const halfedge_descriptor& h) const + FT sqlength(const halfedge_descriptor& h, const PolygonMesh& pmesh) const { - return sqlength(target(h, m_pmesh), source(h, m_pmesh)); + return sqlength(target(h, pmesh), source(h, pmesh)); } public: - boost::optional is_too_long(const halfedge_descriptor h) const + boost::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - const FT sqlen = sqlength(h); + const FT sqlen = sqlength(h, pmesh); if(sqlen > m_sq_long) return sqlen; else @@ -75,29 +80,25 @@ class Uniform_sizing_field : public CGAL::Sizing_field return boost::none; } - boost::optional is_too_short(const halfedge_descriptor h) const + boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - const FT sqlen = sqlength(h); + const FT sqlen = sqlength(h, pmesh); if (sqlen < m_sq_long) return sqlen; else return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor h) const + virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - typename boost::property_map::const_type - vpmap = get(CGAL::vertex_point, m_pmesh); - return CGAL::midpoint(get(vpmap, target(h, m_pmesh)), - get(vpmap, source(h, m_pmesh))); + return CGAL::midpoint(get(m_vpmap, target(h, pmesh)), + get(m_vpmap, source(h, pmesh))); } - const PolygonMesh& get_mesh() const { return m_pmesh; } - private: FT m_sq_short; FT m_sq_long; - const PolygonMesh& m_pmesh; + const VPMap m_vpmap; }; }//end namespace Polygon_mesh_processing diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index b60148c3e73d..d4109708352b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -23,14 +23,17 @@ namespace CGAL /*! * Sizing field virtual class */ -template +template ::const_type> class Sizing_field { private: typedef PolygonMesh PM; - typedef typename boost::property_map::const_type VPMap; typedef typename boost::property_traits::value_type Point; +protected: + typedef typename boost::property_map::const_type DefaultVPMap; + public: typedef typename CGAL::Kernel_traits::Kernel K; typedef typename boost::graph_traits::face_descriptor face_descriptor; @@ -40,12 +43,13 @@ class Sizing_field typedef typename K::FT FT; public: - virtual boost::optional is_too_long(const halfedge_descriptor h) const = 0; + virtual boost::optional is_too_long(const halfedge_descriptor h, + const PolygonMesh& pmesh) const = 0; virtual boost::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const = 0; - virtual boost::optional is_too_short(const halfedge_descriptor h) const = 0; - virtual Point_3 split_placement(const halfedge_descriptor h) const = 0; - virtual const PolygonMesh& get_mesh() const = 0; + virtual boost::optional is_too_short(const halfedge_descriptor h, + const PolygonMesh& pmesh) const = 0; + virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; }; 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 bb65af3e2d10..27ea3cd29271 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 @@ -78,7 +78,7 @@ namespace CGAL { namespace Polygon_mesh_processing { -template class Uniform_sizing_field; +template class Uniform_sizing_field; namespace internal { @@ -404,7 +404,7 @@ namespace internal { ); for(edge_descriptor e : edge_range) { - boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_)); + boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); if(sqlen != boost::none) long_edges.emplace(halfedge(e, mesh_), sqlen.get()); } @@ -438,16 +438,16 @@ namespace internal { std::cout << " refinement point : " << refinement_point << std::endl; #endif //update sizing field with the new point - if constexpr (!std::is_same_v>) - sizing.update_sizing_map(vnew); + if constexpr (!std::is_same_v>) + sizing.update_sizing_map(vnew, mesh_); //check sub-edges //if it was more than twice the "long" threshold, insert them - boost::optional sqlen_new = sizing.is_too_long(hnew); + boost::optional sqlen_new = sizing.is_too_long(hnew, mesh_); if(sqlen_new != boost::none) long_edges.emplace(hnew, sqlen_new.get()); - sqlen_new = sizing.is_too_long(next(hnew, mesh_)); + sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); if (sqlen_new != boost::none) long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); @@ -504,7 +504,7 @@ namespace internal { { if (!is_split_allowed(e)) continue; - boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_)); + boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); if(sqlen != boost::none) long_edges.emplace(halfedge(e, mesh_), sqlen.get()); } @@ -535,7 +535,7 @@ namespace internal { Patch_id patch_id_opp = get_patch_id(face(opposite(he, mesh_), mesh_)); //split edge - Point refinement_point = sizing.split_placement(he); + Point refinement_point = sizing.split_placement(he, mesh_); halfedge_descriptor hnew = CGAL::Euler::split_edge(he, mesh_); CGAL_assertion(he == next(hnew, mesh_)); put(ecmap_, edge(hnew, mesh_), get(ecmap_, edge(he, mesh_)) ); @@ -555,16 +555,16 @@ namespace internal { halfedge_added(hnew_opp, status(opposite(he, mesh_))); //update sizing field with the new point - if constexpr (!std::is_same_v>) - sizing.update_sizing_map(vnew); + if constexpr (!std::is_same_v>) + sizing.update_sizing_map(vnew, mesh_); //check sub-edges //if it was more than twice the "long" threshold, insert them - boost::optional sqlen_new = sizing.is_too_long(hnew); + boost::optional sqlen_new = sizing.is_too_long(hnew, mesh_); if(sqlen_new != boost::none) long_edges.emplace(hnew, sqlen_new.get()); - sqlen_new = sizing.is_too_long(next(hnew, mesh_)); + sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); if (sqlen_new != boost::none) long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); @@ -585,7 +585,7 @@ namespace internal { if (snew == PATCH) { - boost::optional sql = sizing.is_too_long(hnew2); + boost::optional sql = sizing.is_too_long(hnew2, mesh_); if(sql != boost::none) long_edges.emplace(hnew2, sql.get()); } @@ -608,7 +608,7 @@ namespace internal { if (snew == PATCH) { - boost::optional sql = sizing.is_too_long(hnew2); + boost::optional sql = sizing.is_too_long(hnew2, mesh_); if (sql != boost::none) long_edges.emplace(hnew2, sql.get()); } @@ -653,7 +653,7 @@ namespace internal { Boost_bimap short_edges; for(edge_descriptor e : edges(mesh_)) { - boost::optional sqlen = sizing.is_too_short(halfedge(e, mesh_)); + boost::optional sqlen = sizing.is_too_short(halfedge(e, mesh_), mesh_); if(sqlen != boost::none && is_collapse_allowed(e, collapse_constraints)) short_edges.insert(short_edge(halfedge(e, mesh_), sqlen.get())); @@ -818,7 +818,7 @@ namespace internal { //insert new/remaining short edges for (halfedge_descriptor ht : halfedges_around_target(vkept, mesh_)) { - boost::optional sqlen = sizing.is_too_short(ht); + boost::optional sqlen = sizing.is_too_short(ht, mesh_); if (sqlen != boost::none && is_collapse_allowed(edge(ht, mesh_), collapse_constraints)) short_edges.insert(short_edge(ht, sqlen.get())); @@ -1053,7 +1053,7 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); - if constexpr (std::is_same>::value) + if constexpr (std::is_same>::value) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " using tangential relaxation with weights equal to 1"; @@ -1768,7 +1768,7 @@ namespace internal { //insert new edges in 'short_edges' if (is_collapse_allowed(edge(hf, mesh_), collapse_constraints)) { - boost::optional sqlen = sizing.is_too_short(hf); + boost::optional sqlen = sizing.is_too_short(hf, mesh_); if (sqlen != boost::none) short_edges.insert(typename Bimap::value_type(hf, sqlen.get())); } 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 d146fb57597e..70771b6c81bf 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -206,10 +206,6 @@ void isotropic_remeshing(const FaceRange& faces if (std::begin(faces)==std::end(faces)) return; - //todo ip: precondition or something else? - CGAL_precondition_msg(&(sizing.get_mesh()) == &pmesh, "Input mesh is not the same " - "as the one used for the sizing field!"); - typedef PolygonMesh PM; typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename boost::graph_traits::edge_descriptor edge_descriptor; @@ -348,7 +344,7 @@ void isotropic_remeshing(const FaceRange& faces , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { - Uniform_sizing_field sizing(target_edge_length, pmesh); + Uniform_sizing_field sizing(target_edge_length, pmesh); if (target_edge_length > 0) isotropic_remeshing(faces, sizing, pmesh, np); else @@ -471,7 +467,7 @@ void split_long_edges(const EdgeRange& edges false/*need aabb_tree*/); // check if sizing field needs updating - if constexpr (!std::is_same_v>) + if constexpr (!std::is_same_v>) { //todo ip: check if sizing field needs to be checked and updated here } diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 940d576735e4..1185567f05ab 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -698,7 +698,7 @@ public Q_SLOTS: , CGAL::parameters::geom_traits(EPICK()) . edge_is_constrained_map(eif)); } - else if (edge_sizing_type = 1) + else if (edge_sizing_type == 1) { std::pair edge_min_max{min_length, max_length}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol From ff4bbaa1554bd33233a08dcd6a35ffa4b0583e64 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 15 Aug 2023 20:10:39 +0200 Subject: [PATCH 047/124] Target length fix --- .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 2e047548adaa..0a259d3ef31a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -83,7 +83,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); - if (sqlen < m_sq_long) + if (sqlen < m_sq_short) return sqlen; else return boost::none; From e3727e4a88c318bdd97b20e6fd55f8ebd750587d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 15 Aug 2023 20:14:11 +0200 Subject: [PATCH 048/124] Remove todos --- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 2 +- .../demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index 01f97b210cb8..69b2afaaca01 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -51,7 +51,7 @@ int main(int argc, char* argv[]) if (PMP::is_degenerate_triangle_face(face, mesh)) { std::cout << "Found degenerate triangle!" << std::endl; - continue; //todo ip should something be done about this? + continue; } // Calculate Q(t) triangle quality indicator diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 1185567f05ab..3f1816d7ce31 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -1289,7 +1289,6 @@ public Q_SLOTS: ui.edgeLength_dspinbox->setValue(0.05 * diago_length); - //todo ip - check and adjust these ui.errorTol_edit->setValue(0.001 * diago_length); ui.minEdgeLength_edit->setValue(0.001 * diago_length); ui.maxEdgeLength_edit->setValue(0.5 * diago_length); From 98a3f14c7382cf23e26f0cc8fb8e5ab46b2dc6cf Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 16 Aug 2023 18:55:15 +0200 Subject: [PATCH 049/124] Add PMPSizingField concept to docs --- .../Concepts/PMPSizingField.h | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h new file mode 100644 index 000000000000..5304beb32205 --- /dev/null +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -0,0 +1,59 @@ +/// \ingroup PkgPolygonMeshProcessingConcepts +/// \cgalConcept +/// +/// The concept `PMPSizingField` defines the requirements for the sizing field +/// used in `CGAL::Polygon_mesh_processing::isotropic_remeshing()` to define +/// the target length for every individual edge during the remeshing process. + +class PMPSizingField{ +public: + +/// @name Types +/// @{ + +/// Vertex descriptor type +typedef unspecified_type vertex_descriptor; + +/// Halfedge descriptor type +typedef unspecified_type halfedge_descriptor; + +/// 3D point type +typedef unspecified_type Point_3; + +/// Polygon mesh type +typedef unspecified_type PolygonMesh; + +/// Numerical type +typedef unspecified_type FT; + +/// @} + +/// @name Functions +/// @{ + +/// called to check whether the halfedge 'h' is longer than the target edge size +/// and as such should be split. If the halfedge is longer, it returns the squared +/// length of the edge. +boost::optional is_too_long(const halfedge_descriptor h, + const PolygonMesh& pmesh) const; + +/// called to check whether the halfedge with end vertices 'va' and 'vb' is longer +/// than the target edge size and as such should be split. If the halfedge is longer, +/// it returns the squared length of the edge. +boost::optional is_too_long(const vertex_descriptor va, + const vertex_descriptor vb) const; + +/// called to check whether the halfedge 'h' should be collapsed in case it is +/// shorter than the target edge size +boost::optional is_too_short(const halfedge_descriptor h, + const PolygonMesh& pmesh) const; + +/// called to define the location of the halfedge 'h' split in case 'is_too_long' +/// returns a value +Point_3 split_placement(const halfedge_descriptor h, + const PolygonMesh& pmesh) const; + +/// @} +}; + + From 9e91abb5398fe9a94ac08ced0fdc4c80f7213142 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 16 Aug 2023 20:18:22 +0200 Subject: [PATCH 050/124] First attempt at sizing field docs --- .../Adaptive_sizing_field.h | 33 ++++++++++++++ .../Uniform_sizing_field.h | 45 +++++++++++++++++-- .../tangential_relaxation.h | 1 + 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 1e8151c14b32..1c6915b4c96d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -25,6 +25,22 @@ namespace CGAL { namespace Polygon_mesh_processing { +/*! +* \ingroup PMP_meshing_grp +* provides a set of instructions for isotropic remeshing to achieve variable +* mesh edge lengths as a function of local discrete curvatures. +* +* Edges longer than the local target edge length are split in half, while +* edges shorter than the local target edge length are collapsed. +* +* \cgalModels PMPSizingField +* +* \sa `isotropic_remeshing` +* \sa `Uniform_sizing_field` +* +* @tparam PolygonMesh model of `MutableFaceGraph` that +* has an internal property map for `CGAL::vertex_point_t`. +*/ template class Adaptive_sizing_field : public CGAL::Sizing_field { @@ -44,6 +60,22 @@ class Adaptive_sizing_field : public CGAL::Sizing_field typedef typename boost::property_map::type VertexSizingMap; + /// \name Creation + /// @{ + /*! + * Returns an object to serve as criteria for adaptive curvature-based edge lengths. + * + * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, + * model of `Range`. Its iterator type is `ForwardIterator`. + * + * @param tol the error tolerance, the maximum deviation of an edge from the original + * mesh. Lower tolerance values will result in shorter mesh edges. + * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed + * edge length. + * @param face_range the range of triangular faces defining one or several surface patches + * to be remeshed. + * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. + */ template Adaptive_sizing_field(const double tol , const std::pair& edge_len_min_max @@ -74,6 +106,7 @@ class Adaptive_sizing_field : public CGAL::Sizing_field calc_sizing_map(ffg); } } + ///@} private: template diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 0a259d3ef31a..956ebeb21706 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -23,6 +23,25 @@ namespace CGAL { namespace Polygon_mesh_processing { +/*! +* \ingroup PMP_meshing_grp +* provides a set of instructions for isotropic remeshing to achieve uniform +* mesh edge lengths. +* +* Edges longer than the 4/3 of the target edge length will be split in half, while +* edges shorter than the 4/5 of the target edge length will be collapsed. +* +* \cgalModels PMPSizingField +* +* \sa `isotropic_remeshing` +* \sa `Adaptive_sizing_field` +* +* @tparam PolygonMesh model of `MutableFaceGraph` that +* has an internal property map for `CGAL::vertex_point_t`. +* @tparam VPMap a property map associating points to the vertices of `pmesh`. +* It is a a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. +*/ template ::const_type> class Uniform_sizing_field : public CGAL::Sizing_field @@ -36,18 +55,36 @@ class Uniform_sizing_field : public CGAL::Sizing_field typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; - Uniform_sizing_field(const FT size, const VPMap& vpmap) + /// \name Creation + /// @{ + /*! + * Returns an object to serve as criterion for uniform edge lengths. + * + * @param size is the target edge length for the isotropic remeshing. If set to 0, + * the criterion for edge length is ignored and edges are neither split nor collapsed. + * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. + */ + Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) : m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) - , m_vpmap(vpmap) + , m_vpmap(get(CGAL::vertex_point, pmesh)) {} - Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + /*! + * Returns an object to serve as criterion for uniform edge lengths. + * \param size is the target edge length for the isotropic remeshing. If set to 0, + * the criterion for edge length is ignored and edges are neither split nor collapsed. + * \param vpmap is the input vertex point map that associates points to the vertices of + * an input mesh. + */ + Uniform_sizing_field(const FT size, const VPMap& vpmap) : m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) - , m_vpmap(get(CGAL::vertex_point, pmesh)) + , m_vpmap(vpmap) {} + /// @} + private: FT sqlength(const vertex_descriptor va, const vertex_descriptor vb) const diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index c0103f943299..54b614304e19 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -317,6 +317,7 @@ void tangential_relaxation(const VertexRange& vertices, #endif } +//todo ip: doc here? template Date: Thu, 17 Aug 2023 00:29:19 +0200 Subject: [PATCH 051/124] Fix templating error in isotropic remeshing overload --- .../include/CGAL/Polygon_mesh_processing/remesh.h | 9 ++++++++- .../Plugins/PMP/Isotropic_remeshing_plugin.cpp | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) 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 70771b6c81bf..bee2a645b2c4 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -344,7 +344,14 @@ void isotropic_remeshing(const FaceRange& faces , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { - Uniform_sizing_field sizing(target_edge_length, pmesh); + using parameters::choose_parameter; + using parameters::get_parameter; + + typedef typename GetVertexPointMap::type VPMap; + VPMap vpmap = choose_parameter(get_parameter(np, internal_np::vertex_point), + get_property_map(vertex_point, pmesh)); + + Uniform_sizing_field sizing(target_edge_length, vpmap); if (target_edge_length > 0) isotropic_remeshing(faces, sizing, pmesh, np); else diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 3f1816d7ce31..a819c2c22943 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -435,7 +435,7 @@ public Q_SLOTS: selection_item->constrained_edges_pmap(), get(CGAL::vertex_point, *selection_item->polyhedron()), CGAL::Constant_property_map(1), - CGAL::Polygon_mesh_processing::Uniform_sizing_field( 4. / 3. * target_length, pmesh))) + CGAL::Polygon_mesh_processing::Uniform_sizing_field( 4. / 3. * target_length, pmesh))) { QApplication::restoreOverrideCursor(); //If facets are selected, splitting edges will add facets that won't be selected, and it will mess up the rest. @@ -741,7 +741,7 @@ public Q_SLOTS: ecm, get(CGAL::vertex_point, pmesh), CGAL::Constant_property_map(1), - CGAL::Polygon_mesh_processing::Uniform_sizing_field(4. / 3. * target_length, pmesh))) + CGAL::Polygon_mesh_processing::Uniform_sizing_field(4. / 3. * target_length, pmesh))) { QApplication::restoreOverrideCursor(); QMessageBox::warning(mw, tr("Error"), From f5d23db40a57b816024edb19ee483aaf7f40ae37 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 17 Aug 2023 08:03:37 +0200 Subject: [PATCH 052/124] Add template argument to constructor --- .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 956ebeb21706..ce80e3edd0a6 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -64,7 +64,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field * the criterion for edge length is ignored and edges are neither split nor collapsed. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. */ - Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) : m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) , m_vpmap(get(CGAL::vertex_point, pmesh)) From 1f9142bfc2e116623c364c1ff884e4533dfaf284 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 17 Aug 2023 18:19:03 +0200 Subject: [PATCH 053/124] Try to fix failing test --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 5 +++-- .../internal/Isotropic_remeshing/Sizing_field.h | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 1c6915b4c96d..2914a1c36053 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -41,8 +41,9 @@ namespace Polygon_mesh_processing * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. */ -template -class Adaptive_sizing_field : public CGAL::Sizing_field +template ::const_type> +class Adaptive_sizing_field : public CGAL::Sizing_field { private: typedef CGAL::Sizing_field Base; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index d4109708352b..331e2ab922d0 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -16,6 +16,8 @@ #include #include +#include + #include namespace CGAL From 46b50511a70fdd06405344128ba9fc63cfb9fad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 18 Aug 2023 10:16:45 +0200 Subject: [PATCH 054/124] add missing include directive detected by the CI --- .../Adaptive_sizing_field.h | 16 ++++++++++------ .../internal/Isotropic_remeshing/Sizing_field.h | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 2914a1c36053..06b662afbe04 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -16,11 +16,15 @@ #include #include - #include +#include +#include + #include + + namespace CGAL { namespace Polygon_mesh_processing @@ -100,9 +104,9 @@ class Adaptive_sizing_field : public CGAL::Sizing_field auto is_selected = get(CGAL::dynamic_face_property_t(), pmesh); for (face_descriptor f : faces(pmesh)) put(is_selected, f, false); for (face_descriptor f : face_range) put(is_selected, f, true); - CGAL::expand_face_selection(selection, pmesh, 1 - , is_selected, std::back_inserter(selection)); - CGAL::Face_filtered_graph ffg(pmesh, selection); + expand_face_selection(selection, pmesh, 1, + is_selected, std::back_inserter(selection)); + Face_filtered_graph ffg(pmesh, selection); calc_sizing_map(ffg); } @@ -224,8 +228,8 @@ class Adaptive_sizing_field : public CGAL::Sizing_field virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - return CGAL::midpoint(get(m_vpmap, target(h, pmesh)), - get(m_vpmap, source(h, pmesh))); + return midpoint(get(m_vpmap, target(h, pmesh)), + get(m_vpmap, source(h, pmesh))); } void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h index 331e2ab922d0..8267df50f556 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include From e845b07bab5a8d3d11dd1a574385bc2658276869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 18 Aug 2023 10:51:56 +0200 Subject: [PATCH 055/124] using Koening lookup --- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index ce80e3edd0a6..fb88321d6a2c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -89,7 +89,7 @@ class Uniform_sizing_field : public CGAL::Sizing_field FT sqlength(const vertex_descriptor va, const vertex_descriptor vb) const { - return FT(CGAL::squared_distance(get(m_vpmap, va), get(m_vpmap, vb))); + return FT(squared_distance(get(m_vpmap, va), get(m_vpmap, vb))); } FT sqlength(const halfedge_descriptor& h, const PolygonMesh& pmesh) const @@ -128,8 +128,8 @@ class Uniform_sizing_field : public CGAL::Sizing_field virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { - return CGAL::midpoint(get(m_vpmap, target(h, pmesh)), - get(m_vpmap, source(h, pmesh))); + return midpoint(get(m_vpmap, target(h, pmesh)), + get(m_vpmap, source(h, pmesh))); } private: From b69a2671fe9b0d040fc6123993d5622a2a6d357f Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 18 Aug 2023 17:45:59 +0200 Subject: [PATCH 056/124] Rename Sizing_field to Sizing_field_base --- .../Polygon_mesh_processing/Adaptive_sizing_field.h | 11 ++++------- .../Polygon_mesh_processing/Uniform_sizing_field.h | 6 +++--- .../{Sizing_field.h => Sizing_field_base.h} | 2 +- .../include/CGAL/Polygon_mesh_processing/remesh.h | 4 ++-- 4 files changed, 10 insertions(+), 13 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/{Sizing_field.h => Sizing_field_base.h} (98%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 06b662afbe04..60b5e3a8855f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include #include @@ -23,8 +23,6 @@ #include - - namespace CGAL { namespace Polygon_mesh_processing @@ -45,12 +43,11 @@ namespace Polygon_mesh_processing * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. */ -template ::const_type> -class Adaptive_sizing_field : public CGAL::Sizing_field +template +class Adaptive_sizing_field : public CGAL::Sizing_field_base { private: - typedef CGAL::Sizing_field Base; + typedef CGAL::Sizing_field_base Base; public: typedef typename Base::K K; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index fb88321d6a2c..d22e0079d7fd 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include @@ -44,10 +44,10 @@ namespace Polygon_mesh_processing */ template ::const_type> -class Uniform_sizing_field : public CGAL::Sizing_field +class Uniform_sizing_field : public CGAL::Sizing_field_base { private: - typedef CGAL::Sizing_field Base; + typedef CGAL::Sizing_field_base Base; public: typedef typename Base::FT FT; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h similarity index 98% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 8267df50f556..81afe3af21fa 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -27,7 +27,7 @@ namespace CGAL */ template ::const_type> -class Sizing_field +class Sizing_field_base { private: typedef PolygonMesh PM; 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 bee2a645b2c4..842d980eae80 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -45,7 +45,7 @@ namespace Polygon_mesh_processing { * models of `Hashable`. * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, * model of `Range`. Its iterator type is `ForwardIterator`. -* @tparam SizingFunction model of `Sizing_field` +* @tparam SizingFunction model of `PMPSizingField` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param pmesh a polygon mesh with triangulated surface patches to be remeshed @@ -372,7 +372,7 @@ void isotropic_remeshing(const FaceRange& faces * has an internal property map for `CGAL::vertex_point_t`. * @tparam EdgeRange range of `boost::graph_traits::%edge_descriptor`, * model of `Range`. Its iterator type is `InputIterator`. -* @tparam SizingFunction model of `Sizing_field` +* @tparam SizingFunction model of `Sizing_field_base` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param pmesh a polygon mesh From 35153d509d2efc054be9a3ec67069d1a105b12de Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 18 Aug 2023 19:07:07 +0200 Subject: [PATCH 057/124] Update documentation --- .../Concepts/PMPSizingField.h | 13 ++- .../CGAL/Polygon_mesh_processing/remesh.h | 2 +- .../tangential_relaxation.h | 85 ++++++++++++++++++- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index 5304beb32205..da58562c5577 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -31,28 +31,27 @@ typedef unspecified_type FT; /// @name Functions /// @{ -/// called to check whether the halfedge 'h' is longer than the target edge size +/// called to check whether the halfedge `h` is longer than the target edge size /// and as such should be split. If the halfedge is longer, it returns the squared /// length of the edge. boost::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// called to check whether the halfedge with end vertices 'va' and 'vb' is longer +/// called to check whether the halfedge with end vertices `va` and `vb` is longer /// than the target edge size and as such should be split. If the halfedge is longer, /// it returns the squared length of the edge. boost::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const; -/// called to check whether the halfedge 'h' should be collapsed in case it is -/// shorter than the target edge size +/// called to check whether the halfedge `h` should be collapsed in case it is +/// shorter than the target edge size. boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// called to define the location of the halfedge 'h' split in case 'is_too_long' -/// returns a value +/// called to define the location of the halfedge `h` split in case `is_too_long` +/// returns a value. Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const; - /// @} }; 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 842d980eae80..db036a4be6ac 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -372,7 +372,7 @@ void isotropic_remeshing(const FaceRange& faces * has an internal property map for `CGAL::vertex_point_t`. * @tparam EdgeRange range of `boost::graph_traits::%edge_descriptor`, * model of `Range`. Its iterator type is `InputIterator`. -* @tparam SizingFunction model of `Sizing_field_base` +* @tparam SizingFunction model of `PMPSizingField` * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param pmesh a polygon mesh diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 54b614304e19..db4c3f9955d9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -317,7 +317,90 @@ void tangential_relaxation(const VertexRange& vertices, #endif } -//todo ip: doc here? +/*! +* \ingroup PMP_meshing_grp +* applies an iterative area-based tangential smoothing to the given range of vertices based on the +* underlying sizing field. +* Each vertex `v` is relocated to its weighted centroid, where weights depend on the area of the +* adjacent triangle and its averaged vertex sizing values. +* The relocation vector is projected back to the tangent plane to the surface at `v`, iteratively. +* The connectivity remains unchanged. +* +* @tparam TriangleMesh model of `FaceGraph` and `VertexListGraph`. +* The descriptor types `boost::graph_traits::%face_descriptor` +* and `boost::graph_traits::%halfedge_descriptor` must be +* models of `Hashable`. +* @tparam VertexRange range of `boost::graph_traits::%vertex_descriptor`, +* model of `Range`. Its iterator type is `ForwardIterator`. +* @tparam SizingFunction model of `PMPSizingField` +* @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" +* +* @param vertices the range of vertices which will be relocated by relaxation +* @param tm the triangle mesh to which `vertices` belong +* @param sizing a map containing sizing field for individual vertices. +* Used to derive smoothing weights. +* @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below +* +* \cgalNamedParamsBegin +* \cgalParamNBegin{vertex_point_map} +* \cgalParamDescription{a property map associating points to the vertices of `tm`} +* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* as key type and `%Point_3` as value type} +* \cgalParamDefault{`boost::get(CGAL::vertex_point, tm)`} +* \cgalParamExtra{If this parameter is omitted, an internal property map for `CGAL::vertex_point_t` +* must be available in `TriangleMesh`.} +* \cgalParamNEnd +* +* \cgalParamNBegin{geom_traits} +* \cgalParamDescription{an instance of a geometric traits class} +* \cgalParamType{a class model of `Kernel`} +* \cgalParamDefault{a \cgal Kernel deduced from the `Point_3` type, using `CGAL::Kernel_traits`} +* \cgalParamExtra{The geometric traits class must be compatible with the vertex `Point_3` type.} +* \cgalParamExtra{Exact constructions kernels are not supported by this function.} +* \cgalParamNEnd +* +* \cgalParamNBegin{number_of_iterations} +* \cgalParamDescription{the number of smoothing iterations} +* \cgalParamType{unsigned int} +* \cgalParamDefault{`1`} +* \cgalParamNEnd +* +* \cgalParamNBegin{edge_is_constrained_map} +* \cgalParamDescription{a property map containing the constrained-or-not status of each edge of `tm`. +* The endpoints of a constrained edge cannot be moved by relaxation.} +* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%edge_descriptor` +* as key type and `bool` as value type. It must be default constructible.} +* \cgalParamDefault{a default property map where no edges are constrained} +* \cgalParamExtra{Boundary edges are always considered as constrained edges.} +* \cgalParamNEnd +* +* \cgalParamNBegin{vertex_is_constrained_map} +* \cgalParamDescription{a property map containing the constrained-or-not status of each vertex of `tm`. +* A constrained vertex cannot be modified during relaxation.} +* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* as key type and `bool` as value type. It must be default constructible.} +* \cgalParamDefault{a default property map where no vertices are constrained} +* \cgalParamNEnd +* +* \cgalParamNBegin{relax_constraints} +* \cgalParamDescription{If `true`, the end vertices of the edges set as constrained +* in `edge_is_constrained_map` and boundary edges move along the +* constrained polylines they belong to.} +* \cgalParamType{Boolean} +* \cgalParamDefault{`false`} +* \cgalParamNEnd +* +* \cgalParamNBegin{allow_move_functor} +* \cgalParamDescription{A function object used to determinate if a vertex move should be allowed or not} +* \cgalParamType{Unary functor that provides `bool operator()(vertex_descriptor v, Point_3 src, Point_3 tgt)` returning `true` +* if the vertex `v` can be moved from `src` to `tgt`; `Point_3` being the value type of the vertex point map } +* \cgalParamDefault{If not provided, all moves are allowed.} +* \cgalParamNEnd +* +* \cgalNamedParamsEnd +* +* \todo check if it should really be a triangle mesh or if a polygon mesh is fine +*/ template Date: Fri, 18 Aug 2023 20:17:38 +0200 Subject: [PATCH 058/124] Add precondition that sizing field and remeshing vpmap must be the same --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 +++- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 4 +++- .../internal/Isotropic_remeshing/Sizing_field_base.h | 2 ++ .../include/CGAL/Polygon_mesh_processing/remesh.h | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 60b5e3a8855f..36f714dab6ef 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -223,12 +223,14 @@ class Adaptive_sizing_field : public CGAL::Sizing_field_base return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const + Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { return midpoint(get(m_vpmap, target(h, pmesh)), get(m_vpmap, source(h, pmesh))); } + const DefaultVPMap& get_vpmap() const { return m_vpmap; } + void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) { // calculating it as the average of two vertices on other ends diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index d22e0079d7fd..46ca4c5acedc 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -126,12 +126,14 @@ class Uniform_sizing_field : public CGAL::Sizing_field_base return boost::none; } - virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const + Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const { return midpoint(get(m_vpmap, target(h, pmesh)), get(m_vpmap, source(h, pmesh))); } + const VPMap& get_vpmap() const { return m_vpmap; } + private: FT m_sq_short; FT m_sq_long; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 81afe3af21fa..13661883b2aa 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -53,6 +53,8 @@ class Sizing_field_base const PolygonMesh& pmesh) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; + virtual const VPMap& get_vpmap() const = 0; + }; }//end namespace CGAL 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 db036a4be6ac..a36c3248db12 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -277,6 +277,9 @@ void isotropic_remeshing(const FaceRange& faces } #endif + CGAL_precondition_msg((sizing.get_vpmap()) == vpmap, "Input mesh/vertex point map is not the same as the " + "one used for sizing field. Remeshing aborted."); + #ifdef CGAL_PMP_REMESHING_VERBOSE t.stop(); std::cout << "\rRemeshing parameters done ("<< t.time() <<" sec)" << std::endl; From a96054a05151983dcd7a00b7afe681cf75768b01 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 18 Aug 2023 21:43:36 +0200 Subject: [PATCH 059/124] Place Sizing_field_base under PMP namespace as Uniform and Adaptive classes --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 4 ++-- .../internal/Isotropic_remeshing/Sizing_field_base.h | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 36f714dab6ef..b2444f258c9b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -44,10 +44,10 @@ namespace Polygon_mesh_processing * has an internal property map for `CGAL::vertex_point_t`. */ template -class Adaptive_sizing_field : public CGAL::Sizing_field_base +class Adaptive_sizing_field : public Sizing_field_base { private: - typedef CGAL::Sizing_field_base Base; + typedef Sizing_field_base Base; public: typedef typename Base::K K; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 46ca4c5acedc..1b3c3b3f60b1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -44,10 +44,10 @@ namespace Polygon_mesh_processing */ template ::const_type> -class Uniform_sizing_field : public CGAL::Sizing_field_base +class Uniform_sizing_field : public Sizing_field_base { private: - typedef CGAL::Sizing_field_base Base; + typedef Sizing_field_base Base; public: typedef typename Base::FT FT; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 13661883b2aa..2ab66f8532a1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -22,6 +22,8 @@ namespace CGAL { +namespace Polygon_mesh_processing +{ /*! * Sizing field virtual class */ @@ -57,6 +59,7 @@ class Sizing_field_base }; +}//end namespace Polygon_mesh_processing }//end namespace CGAL #endif //CGAL_PMP_REMESHING_SIZING_FIELD_H From 8cd75d86f7ea01a8efc94c35d3f07994e8fa3380 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sun, 20 Aug 2023 21:24:38 +0200 Subject: [PATCH 060/124] Fix vpmap return error --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- .../internal/Isotropic_remeshing/Sizing_field_base.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index b2444f258c9b..236ac2fb6a14 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -229,7 +229,7 @@ class Adaptive_sizing_field : public Sizing_field_base get(m_vpmap, source(h, pmesh))); } - const DefaultVPMap& get_vpmap() const { return m_vpmap; } + DefaultVPMap get_vpmap() const { return m_vpmap; } void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) { diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 1b3c3b3f60b1..0b45fecb7689 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -132,7 +132,7 @@ class Uniform_sizing_field : public Sizing_field_base get(m_vpmap, source(h, pmesh))); } - const VPMap& get_vpmap() const { return m_vpmap; } + VPMap get_vpmap() const { return m_vpmap; } private: FT m_sq_short; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 2ab66f8532a1..89337716c94f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -55,7 +55,7 @@ class Sizing_field_base const PolygonMesh& pmesh) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; - virtual const VPMap& get_vpmap() const = 0; + virtual VPMap get_vpmap() const = 0; }; From a00509ea477e52494a7bb97985aa8c3bda4fa70e Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 21 Aug 2023 20:05:06 +0200 Subject: [PATCH 061/124] Remove precondition for vpmap --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 -- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 -- .../internal/Isotropic_remeshing/Sizing_field_base.h | 2 -- .../include/CGAL/Polygon_mesh_processing/remesh.h | 3 --- 4 files changed, 9 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 236ac2fb6a14..a5783d811019 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -229,8 +229,6 @@ class Adaptive_sizing_field : public Sizing_field_base get(m_vpmap, source(h, pmesh))); } - DefaultVPMap get_vpmap() const { return m_vpmap; } - void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) { // calculating it as the average of two vertices on other ends diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 0b45fecb7689..aebf1d43f4ed 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -132,8 +132,6 @@ class Uniform_sizing_field : public Sizing_field_base get(m_vpmap, source(h, pmesh))); } - VPMap get_vpmap() const { return m_vpmap; } - private: FT m_sq_short; FT m_sq_long; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 89337716c94f..9b8c545c1d32 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -55,8 +55,6 @@ class Sizing_field_base const PolygonMesh& pmesh) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; - virtual VPMap get_vpmap() const = 0; - }; }//end namespace Polygon_mesh_processing 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 a36c3248db12..db036a4be6ac 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -277,9 +277,6 @@ void isotropic_remeshing(const FaceRange& faces } #endif - CGAL_precondition_msg((sizing.get_vpmap()) == vpmap, "Input mesh/vertex point map is not the same as the " - "one used for sizing field. Remeshing aborted."); - #ifdef CGAL_PMP_REMESHING_VERBOSE t.stop(); std::cout << "\rRemeshing parameters done ("<< t.time() <<" sec)" << std::endl; From 9dff62200795f9e5074aa37b1688e1a3191861f4 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 21 Aug 2023 21:10:44 +0200 Subject: [PATCH 062/124] Changes for documentation --- .../Adaptive_sizing_field.h | 18 ++++++------- .../Uniform_sizing_field.h | 27 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index a5783d811019..5b3b8c398932 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -48,6 +48,9 @@ class Adaptive_sizing_field : public Sizing_field_base { private: typedef Sizing_field_base Base; + typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; + typedef typename boost::property_map::type VertexSizingMap; public: typedef typename Base::K K; @@ -58,25 +61,22 @@ class Adaptive_sizing_field : public Sizing_field_base typedef typename Base::vertex_descriptor vertex_descriptor; typedef typename Base::DefaultVPMap DefaultVPMap; - typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; - typedef typename boost::property_map::type VertexSizingMap; - /// \name Creation /// @{ /*! - * Returns an object to serve as criteria for adaptive curvature-based edge lengths. + * returns an object to serve as criteria for adaptive curvature-based edge lengths. * * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, * model of `Range`. Its iterator type is `ForwardIterator`. * - * @param tol the error tolerance, the maximum deviation of an edge from the original - * mesh. Lower tolerance values will result in shorter mesh edges. + * @param tol the error tolerance, used together with curvature to derive target edge length. + * Lower tolerance values will result in shorter mesh edges. * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed * edge length. * @param face_range the range of triangular faces defining one or several surface patches - * to be remeshed. - * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. + * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. + * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the + * same mesh as the one used in `isotropic_remeshing()`. */ template Adaptive_sizing_field(const double tol diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index aebf1d43f4ed..05f3736f961b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -57,21 +57,9 @@ class Uniform_sizing_field : public Sizing_field_base /// \name Creation /// @{ - /*! - * Returns an object to serve as criterion for uniform edge lengths. - * - * @param size is the target edge length for the isotropic remeshing. If set to 0, - * the criterion for edge length is ignored and edges are neither split nor collapsed. - * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. - */ - Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) - : m_sq_short( CGAL::square(4./5. * size)) - , m_sq_long( CGAL::square(4./3. * size)) - , m_vpmap(get(CGAL::vertex_point, pmesh)) - {} /*! - * Returns an object to serve as criterion for uniform edge lengths. + * returns an object to serve as criterion for uniform edge lengths. * \param size is the target edge length for the isotropic remeshing. If set to 0, * the criterion for edge length is ignored and edges are neither split nor collapsed. * \param vpmap is the input vertex point map that associates points to the vertices of @@ -83,6 +71,19 @@ class Uniform_sizing_field : public Sizing_field_base , m_vpmap(vpmap) {} + /*! + * returns an object to serve as criterion for uniform edge lengths. It calls the first + * constructor using default values for the vertex point map of the input polygon mesh. + * + * @param size is the target edge length for the isotropic remeshing. If set to 0, + * the criterion for edge length is ignored and edges are neither split nor collapsed. + * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. The default + * vertex point map of pmesh is used to construct the class. + */ + Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + : Uniform_sizing_field(size, get(CGAL::vertex_point, pmesh)) + {} + /// @} private: From 039b02710ebba36e8c7049b55c9e4a5aca90e9ee Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 21 Aug 2023 22:01:49 +0200 Subject: [PATCH 063/124] boost::optional to std::optional C++ 17 update --- .../Concepts/PMPSizingField.h | 6 +- .../Adaptive_sizing_field.h | 12 ++-- .../Uniform_sizing_field.h | 12 ++-- .../Isotropic_remeshing/Sizing_field_base.h | 12 ++-- .../Isotropic_remeshing/remesh_impl.h | 70 +++++++++---------- 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index da58562c5577..8f89b3a88967 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -34,18 +34,18 @@ typedef unspecified_type FT; /// called to check whether the halfedge `h` is longer than the target edge size /// and as such should be split. If the halfedge is longer, it returns the squared /// length of the edge. -boost::optional is_too_long(const halfedge_descriptor h, +std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const; /// called to check whether the halfedge with end vertices `va` and `vb` is longer /// than the target edge size and as such should be split. If the halfedge is longer, /// it returns the squared length of the edge. -boost::optional is_too_long(const vertex_descriptor va, +std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const; /// called to check whether the halfedge `h` should be collapsed in case it is /// shorter than the target edge size. -boost::optional is_too_short(const halfedge_descriptor h, +std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const; /// called to define the location of the halfedge `h` split in case `is_too_long` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 5b3b8c398932..fcc96deb9baf 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -183,7 +183,7 @@ class Adaptive_sizing_field : public Sizing_field_base return get(m_vertex_sizing_map, v); } - boost::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const + std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), @@ -193,10 +193,10 @@ class Adaptive_sizing_field : public Sizing_field_base if(sqlen > sqtarg_len) return sqlen; else - return boost::none; + return std::nullopt; } - boost::optional is_too_long(const vertex_descriptor va, + std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); @@ -207,10 +207,10 @@ class Adaptive_sizing_field : public Sizing_field_base if (sqlen > sqtarg_len) return sqlen; else - return boost::none; + return std::nullopt; } - boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const + std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); FT sqtarg_len = CGAL::square(4./5. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), @@ -220,7 +220,7 @@ class Adaptive_sizing_field : public Sizing_field_base if (sqlen < sqtarg_len) return sqlen; else - return boost::none; + return std::nullopt; } Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 05f3736f961b..7e8c24f3dd22 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -99,32 +99,32 @@ class Uniform_sizing_field : public Sizing_field_base } public: - boost::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const + std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); if(sqlen > m_sq_long) return sqlen; else - return boost::none; + return std::nullopt; } - boost::optional is_too_long(const vertex_descriptor va, + std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); if (sqlen > m_sq_long) return sqlen; else - return boost::none; + return std::nullopt; } - boost::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const + std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); if (sqlen < m_sq_short) return sqlen; else - return boost::none; + return std::nullopt; } Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 9b8c545c1d32..d433a4c16d61 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -47,12 +47,12 @@ class Sizing_field_base typedef typename K::FT FT; public: - virtual boost::optional is_too_long(const halfedge_descriptor h, - const PolygonMesh& pmesh) const = 0; - virtual boost::optional is_too_long(const vertex_descriptor va, - const vertex_descriptor vb) const = 0; - virtual boost::optional is_too_short(const halfedge_descriptor h, - const PolygonMesh& pmesh) const = 0; + virtual std::optional is_too_long(const halfedge_descriptor h, + const PolygonMesh& pmesh) const = 0; + virtual std::optional is_too_long(const vertex_descriptor va, + const vertex_descriptor vb) const = 0; + virtual std::optional is_too_short(const halfedge_descriptor h, + const PolygonMesh& pmesh) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; }; 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 27ea3cd29271..895083fbbcd3 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 @@ -43,7 +43,6 @@ #include #include #include -#include #include #include @@ -54,6 +53,7 @@ #include #include #include +#include //todo ip: temp #define CGAL_PMP_REMESHING_VERBOSE @@ -404,9 +404,9 @@ namespace internal { ); for(edge_descriptor e : edge_range) { - boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); - if(sqlen != boost::none) - long_edges.emplace(halfedge(e, mesh_), sqlen.get()); + std::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); + if(sqlen != std::nullopt) + long_edges.emplace(halfedge(e, mesh_), sqlen.value()); } //split long edges @@ -443,13 +443,13 @@ namespace internal { //check sub-edges //if it was more than twice the "long" threshold, insert them - boost::optional sqlen_new = sizing.is_too_long(hnew, mesh_); - if(sqlen_new != boost::none) - long_edges.emplace(hnew, sqlen_new.get()); + std::optional sqlen_new = sizing.is_too_long(hnew, mesh_); + if(sqlen_new != std::nullopt) + long_edges.emplace(hnew, sqlen_new.value()); sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); - if (sqlen_new != boost::none) - long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); + if (sqlen_new != std::nullopt) + long_edges.emplace(next(hnew, mesh_), sqlen_new.value()); //insert new edges to keep triangular faces, and update long_edges if (!is_border(hnew, mesh_)) @@ -504,9 +504,9 @@ namespace internal { { if (!is_split_allowed(e)) continue; - boost::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); - if(sqlen != boost::none) - long_edges.emplace(halfedge(e, mesh_), sqlen.get()); + std::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); + if(sqlen != std::nullopt) + long_edges.emplace(halfedge(e, mesh_), sqlen.value()); } //split long edges @@ -560,13 +560,13 @@ namespace internal { //check sub-edges //if it was more than twice the "long" threshold, insert them - boost::optional sqlen_new = sizing.is_too_long(hnew, mesh_); - if(sqlen_new != boost::none) - long_edges.emplace(hnew, sqlen_new.get()); + std::optional sqlen_new = sizing.is_too_long(hnew, mesh_); + if(sqlen_new != std::nullopt) + long_edges.emplace(hnew, sqlen_new.value()); sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); - if (sqlen_new != boost::none) - long_edges.emplace(next(hnew, mesh_), sqlen_new.get()); + if (sqlen_new != std::nullopt) + long_edges.emplace(next(hnew, mesh_), sqlen_new.value()); //insert new edges to keep triangular faces, and update long_edges if (!is_on_border(hnew)) @@ -585,9 +585,9 @@ namespace internal { if (snew == PATCH) { - boost::optional sql = sizing.is_too_long(hnew2, mesh_); - if(sql != boost::none) - long_edges.emplace(hnew2, sql.get()); + std::optional sql = sizing.is_too_long(hnew2, mesh_); + if(sql != std::nullopt) + long_edges.emplace(hnew2, sql.value()); } } @@ -608,9 +608,9 @@ namespace internal { if (snew == PATCH) { - boost::optional sql = sizing.is_too_long(hnew2, mesh_); - if (sql != boost::none) - long_edges.emplace(hnew2, sql.get()); + std::optional sql = sizing.is_too_long(hnew2, mesh_); + if (sql != std::nullopt) + long_edges.emplace(hnew2, sql.value()); } } } @@ -653,10 +653,10 @@ namespace internal { Boost_bimap short_edges; for(edge_descriptor e : edges(mesh_)) { - boost::optional sqlen = sizing.is_too_short(halfedge(e, mesh_), mesh_); - if(sqlen != boost::none + std::optional sqlen = sizing.is_too_short(halfedge(e, mesh_), mesh_); + if(sqlen != std::nullopt && is_collapse_allowed(e, collapse_constraints)) - short_edges.insert(short_edge(halfedge(e, mesh_), sqlen.get())); + short_edges.insert(short_edge(halfedge(e, mesh_), sqlen.value())); } #ifdef CGAL_PMP_REMESHING_VERBOSE_PROGRESS std::cout << "done." << std::endl; @@ -752,8 +752,8 @@ namespace internal { for(halfedge_descriptor ha : halfedges_around_target(va, mesh_)) { vertex_descriptor va_i = source(ha, mesh_); - boost::optional sqha = sizing.is_too_long(vb, va_i); - if (sqha != boost::none) + std::optional sqha = sizing.is_too_long(vb, va_i); + if (sqha != std::nullopt) { collapse_ok = false; break; @@ -818,10 +818,10 @@ namespace internal { //insert new/remaining short edges for (halfedge_descriptor ht : halfedges_around_target(vkept, mesh_)) { - boost::optional sqlen = sizing.is_too_short(ht, mesh_); - if (sqlen != boost::none + std::optional sqlen = sizing.is_too_short(ht, mesh_); + if (sqlen != std::nullopt && is_collapse_allowed(edge(ht, mesh_), collapse_constraints)) - short_edges.insert(short_edge(ht, sqlen.get())); + short_edges.insert(short_edge(ht, sqlen.value())); } } }//end if(collapse_ok) @@ -1768,9 +1768,9 @@ namespace internal { //insert new edges in 'short_edges' if (is_collapse_allowed(edge(hf, mesh_), collapse_constraints)) { - boost::optional sqlen = sizing.is_too_short(hf, mesh_); - if (sqlen != boost::none) - short_edges.insert(typename Bimap::value_type(hf, sqlen.get())); + std::optional sqlen = sizing.is_too_short(hf, mesh_); + if (sqlen != std::nullopt) + short_edges.insert(typename Bimap::value_type(hf, sqlen.value())); } if(!is_border(hf, mesh_) && @@ -1986,7 +1986,7 @@ namespace internal { bool check_normals(const HalfedgeRange& hedges) const { std::size_t nb_patches = patch_id_to_index_map.size(); - //std::vector > normal_per_patch(nb_patches,boost::none); + //std::vector > normal_per_patch(nb_patches,std::nullopt); std::vector initialized(nb_patches,false); std::vector normal_per_patch(nb_patches); From 4ca59942bf63bbde8df5850f5eb34e287ba793f0 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Mon, 21 Aug 2023 22:09:38 +0200 Subject: [PATCH 064/124] Document Sizing_field_base --- .../Isotropic_remeshing/Sizing_field_base.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index d433a4c16d61..55cdfceff24e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -25,7 +25,21 @@ namespace CGAL namespace Polygon_mesh_processing { /*! -* Sizing field virtual class +* \ingroup PMP_meshing_grp +* pure virtual class serving as a base for sizing field classes utilized in isotropic +* remeshing. +* +* \cgalModels PMPSizingField +* +* \sa `isotropic_remeshing` +* \sa `Uniform_sizing_field` +* \sa `Adaptive_sizing_field` +* +* @tparam PolygonMesh model of `MutableFaceGraph` that +* has an internal property map for `CGAL::vertex_point_t`. +* @tparam VPMap a property map associating points to the vertices of `pmesh`. +* It is a a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. */ template ::const_type> From 25c82a2ea29c9c790b4edaa49f2390cf25928df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:28:56 +0200 Subject: [PATCH 065/124] UpdateSizing_field_base docs Co-authored-by: Andreas Fabri --- .../internal/Isotropic_remeshing/Sizing_field_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h index 55cdfceff24e..580601bbaecb 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h @@ -31,7 +31,7 @@ namespace Polygon_mesh_processing * * \cgalModels PMPSizingField * -* \sa `isotropic_remeshing` +* \sa `isotropic_remeshing()` * \sa `Uniform_sizing_field` * \sa `Adaptive_sizing_field` * From 04e3be8b8c9cac56c1a36e771bc49843e3696aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:29:28 +0200 Subject: [PATCH 066/124] Update Concepts/PMPSizingField docs Co-authored-by: Andreas Fabri --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index 8f89b3a88967..afd1db889147 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -48,7 +48,7 @@ std::optional is_too_long(const vertex_descriptor va, std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// called to define the location of the halfedge `h` split in case `is_too_long` +/// called to define the location of the halfedge `h` split in case `is_too_long()` /// returns a value. Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const; From 237e915d2baecc52ea8890cf10a323a555b92459 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 23 Aug 2023 21:00:28 +0200 Subject: [PATCH 067/124] Use any argument convertible to double for overloads in isotropic_remeshing() in split_long_edges() --- .../Uniform_sizing_field.h | 2 +- .../CGAL/Polygon_mesh_processing/remesh.h | 31 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 7e8c24f3dd22..2ef86ab0233c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -80,7 +80,7 @@ class Uniform_sizing_field : public Sizing_field_base * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. The default * vertex point map of pmesh is used to construct the class. */ - Uniform_sizing_field(const FT& size, const PolygonMesh& pmesh) + Uniform_sizing_field(const FT size, const PolygonMesh& pmesh) : Uniform_sizing_field(size, get(CGAL::vertex_point, pmesh)) {} 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 db036a4be6ac..a8da9fd9a019 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -51,7 +51,8 @@ namespace Polygon_mesh_processing { * @param pmesh a polygon mesh with triangulated surface patches to be remeshed * @param faces the range of triangular faces defining one or several surface patches to be remeshed * @param sizing uniform or adaptive sizing field that determines a target length for individual edges. -* If a number is passed, it uses uniform sizing with the number as a target edge length. +* If a float is passed (i.e. sizing is convertible to a double), it will use a `Uniform_sizing_field()` +* with the float as a target edge length. * If `0` is passed then only the edge-flip, tangential relaxation, and projection steps will be done. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * @@ -197,7 +198,8 @@ namespace Polygon_mesh_processing { template + , typename NamedParameters = parameters::Default_named_parameters + , typename = typename std::enable_if_t>> void isotropic_remeshing(const FaceRange& faces , SizingFunction& sizing , PolygonMesh& pmesh @@ -338,9 +340,11 @@ void isotropic_remeshing(const FaceRange& faces // Overload when using target_edge_length for sizing template + , typename SizingValue + , typename NamedParameters = parameters::Default_named_parameters + , typename = typename std::enable_if_t>> void isotropic_remeshing(const FaceRange& faces - , const double target_edge_length + , const SizingValue target_edge_length , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { @@ -377,8 +381,8 @@ void isotropic_remeshing(const FaceRange& faces * * @param pmesh a polygon mesh * @param edges the range of edges to be split if they are longer than given threshold -* @param sizing the sizing function that is used to split edges from 'edges' list. If a number is passed, -* all edges longer than the number are split into sub-edges. +* @param sizing the sizing function that is used to split edges from 'edges' list. If a float is passed (i.e. sizing +* is convertible to a double), it will use a `Uniform_sizing_field()` with the float as a target edge length. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin @@ -424,7 +428,8 @@ void isotropic_remeshing(const FaceRange& faces template + , typename NamedParameters = parameters::Default_named_parameters + , typename = typename std::enable_if_t>> void split_long_edges(const EdgeRange& edges , SizingFunction& sizing , PolygonMesh& pmesh @@ -473,21 +478,17 @@ void split_long_edges(const EdgeRange& edges fimap, false/*need aabb_tree*/); - // check if sizing field needs updating - if constexpr (!std::is_same_v>) - { - //todo ip: check if sizing field needs to be checked and updated here - } - remesher.split_long_edges(edges, sizing); } // Convenience overload when using max_length for sizing template + , typename SizingValue + , typename NamedParameters = parameters::Default_named_parameters + , typename = typename std::enable_if_t>> void split_long_edges(const EdgeRange& edges - , const double max_length + , const SizingValue max_length , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) { From ec1793f54d46112ccc35f94e73cf8518fc5a5b79 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 24 Aug 2023 17:50:16 +0200 Subject: [PATCH 068/124] Documentation update --- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 5 ++++- .../include/CGAL/Polygon_mesh_processing/remesh.h | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 2ef86ab0233c..c49273921074 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -44,7 +44,10 @@ namespace Polygon_mesh_processing */ template ::const_type> -class Uniform_sizing_field : public Sizing_field_base +class Uniform_sizing_field +#ifndef DOXYGEN_RUNNING + : public Sizing_field_base +#endif { private: typedef Sizing_field_base Base; 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 a8da9fd9a019..45ce27304cb8 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -51,8 +51,8 @@ namespace Polygon_mesh_processing { * @param pmesh a polygon mesh with triangulated surface patches to be remeshed * @param faces the range of triangular faces defining one or several surface patches to be remeshed * @param sizing uniform or adaptive sizing field that determines a target length for individual edges. -* If a float is passed (i.e. sizing is convertible to a double), it will use a `Uniform_sizing_field()` -* with the float as a target edge length. +* If a number convertible to a double is passed, it will use a `Uniform_sizing_field()` +* with the number as a target edge length. * If `0` is passed then only the edge-flip, tangential relaxation, and projection steps will be done. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * @@ -381,8 +381,8 @@ void isotropic_remeshing(const FaceRange& faces * * @param pmesh a polygon mesh * @param edges the range of edges to be split if they are longer than given threshold -* @param sizing the sizing function that is used to split edges from 'edges' list. If a float is passed (i.e. sizing -* is convertible to a double), it will use a `Uniform_sizing_field()` with the float as a target edge length. +* @param sizing the sizing function that is used to split edges from 'edges' list. If a number convertible to +* a double is passed, it will use a `Uniform_sizing_field()` with the number as a target edge length. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin From f73e7d4a79a833268f4d134cd2ca877b162ddd15 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Wed, 6 Sep 2023 21:38:01 +0200 Subject: [PATCH 069/124] Add adaptive sizing information to the user manual --- Documentation/doc/biblio/cgal_manual.bib | 10 ++++++++ .../Polygon_mesh_processing.txt | 23 +++++++++++++++--- .../fig/uniform_and_adaptive.png | Bin 0 -> 316564 bytes 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 Polygon_mesh_processing/doc/Polygon_mesh_processing/fig/uniform_and_adaptive.png diff --git a/Documentation/doc/biblio/cgal_manual.bib b/Documentation/doc/biblio/cgal_manual.bib index 89d062ddddf5..713047b1ed50 100644 --- a/Documentation/doc/biblio/cgal_manual.bib +++ b/Documentation/doc/biblio/cgal_manual.bib @@ -3073,6 +3073,16 @@ @article{ecvp-bhhhk-14 bibsource = {dblp computer science bibliography, https://dblp.org/} } +@inproceedings {dunyach2013curvRemesh, + booktitle = {Eurographics 2013 - Short Papers}, + title = {{Adaptive Remeshing for Real-Time Mesh Deformation}}, + author = {Dunyach, Marion and Vanderhaeghe, David and Barthe, Loïc and Botsch, Mario}, + year = {2013}, + publisher = {The Eurographics Association}, + ISSN = {1017-4656}, + DOI = {10.2312/conf/EG2013/short/029-032} +} + @book{botsch2010PMP, title={Polygon mesh processing}, author={M. Botsch and L. Kobbelt and M. Pauly and P. Alliez and B. L{\'e}vy}, 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 8f5863bd1f0f..0f89da2c013f 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 @@ -116,10 +116,19 @@ to the original surface to keep a good approximation of the input. A triangulated region of a polygon mesh can be remeshed using the function `CGAL::Polygon_mesh_processing::isotropic_remeshing()`, as illustrated -by \cgalFigureRef{iso_remeshing}. The algorithm has only two parameters : -the target edge length for the remeshed surface patch, and -the number of iterations of the abovementioned sequence of operations. The bigger -this number, the smoother and closer to target edge length the mesh will be. +by \cgalFigureRef{iso_remeshing}. The algorithm has two parameters: +the sizing field object for the remeshed surface patch, and +the number of iterations of the abovementioned sequence of operations. + +The sizing field establishes the target edge length for the remeshed surface. The sizing field can be uniform or +adaptive. With the uniform sizing field, initiated by the `CGAL::Polygon_mesh_processing::Uniform_sizing_field()` constructor, +all triangle edges are targeted to have equal lengths. On the other hand, with the adaptive sizing field, initiated by +the `CGAL::Polygon_mesh_processing::Adaptive_sizing_field()`, triangle edge lengths depend on the local curvature -- +shorter edges appear in regions with a higher curvature and vice versa. The outline of the adaptive sizing +field algorithm is available in \cgalCite{dunyach2013curvRemesh}. The distinction between uniform and adaptive +sizing fields is depicted in figure \cgalFigureRef{uniform_and_adaptive}. + +As the number of iterations increases, the mesh tends to be smoother and closer to the target edge length. An additional option has been added to \e protect (\e i.\e e. not modify) some given polylines. In some cases, those polylines are too long, and reaching the desired target edge length while protecting them is not @@ -134,6 +143,12 @@ Isotropic remeshing. (a) Triangulated input surface mesh. (d) Surface mesh with the selection uniformly remeshed. \cgalFigureEnd +\cgalFigureBegin{uniform_and_adaptive, uniform_and_adaptive.png} +Sizing fields in isotropic remeshing. +(a) Uniform sizing field. +(b) Curvature-based adaptive sizing field. +\cgalFigureEnd + \paragraph Delaunay-Based Surface Remeshing The mesh generation algorithm implemented in the \ref PkgMesh3 package can be used to remesh a given triangulated surface mesh. The algorithm, based on Delaunay refinement of a restricted Delaunay triangulation, diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/fig/uniform_and_adaptive.png b/Polygon_mesh_processing/doc/Polygon_mesh_processing/fig/uniform_and_adaptive.png new file mode 100644 index 0000000000000000000000000000000000000000..4aca8a398d26d773f009845adefcaa90bc7e5915 GIT binary patch literal 316564 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 #hjnJo7ye52 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

As!dHE7o8jMW_{y1=-@ljrM3 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Ðcz*wyt?!F;-3sdea$IE)D%y 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 O0D-5gpUXO@geCyMVxb5C literal 0 HcmV?d00001 From cd6692661d8c5314ba9409d9a132e1e2f3728fd9 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sat, 9 Sep 2023 11:32:18 +0200 Subject: [PATCH 070/124] Move Sizing_field_base.h out of internal --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../{internal => }/Isotropic_remeshing/Sizing_field_base.h | 0 .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{internal => }/Isotropic_remeshing/Sizing_field_base.h (100%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index fcc96deb9baf..b1794a6a6b77 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h similarity index 100% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Isotropic_remeshing/Sizing_field_base.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index c49273921074..02d21df369be 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include From 22b08dfaf86a0a5792ea6d656b8b34a1b911dced Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sat, 9 Sep 2023 11:46:43 +0200 Subject: [PATCH 071/124] Update documentation Info on curvature calculation in Adaptive_sizing_field Update reference manual welcome page --- .../doc/Polygon_mesh_processing/PackageDescription.txt | 7 +++++-- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index 83929a699818..74bfc24e231b 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -115,8 +115,6 @@ The page \ref bgl_namedparameters "Named Parameters" describes their usage. - `CGAL::Polygon_mesh_processing::split()` \cgalCRPSection{Meshing Functions} -- \link PMP_meshing_grp `CGAL::Polygon_mesh_processing::isotropic_remeshing()` \endlink -- \link PMP_meshing_grp `CGAL::Polygon_mesh_processing::split_long_edges()` \endlink - `CGAL::Polygon_mesh_processing::remesh_planar_patches()` - `CGAL::Polygon_mesh_processing::remesh_almost_planar_patches()` - `CGAL::Polygon_mesh_processing::refine()` @@ -131,9 +129,14 @@ The page \ref bgl_namedparameters "Named Parameters" describes their usage. - `CGAL::Polygon_mesh_processing::smooth_mesh()` (deprecated) - `CGAL::Polygon_mesh_processing::angle_and_area_smoothing()` - `CGAL::Polygon_mesh_processing::tangential_relaxation()` +- `CGAL::Polygon_mesh_processing::tangential_relaxation_with_sizing()` - `CGAL::Polygon_mesh_processing::smooth_shape()` - `CGAL::Polygon_mesh_processing::random_perturbation()` +\cgalCRPSection{Sizing Fields} +- `CGAL::Polygon_mesh_processing::Uniform_sizing_field()` +- `CGAL::Polygon_mesh_processing::Adaptive_sizing_field()` + \cgalCRPSection{Orientation Functions} - `CGAL::Polygon_mesh_processing::orient_polygon_soup()` - `CGAL::Polygon_mesh_processing::orient()` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index b1794a6a6b77..60c9e5f76597 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -32,6 +32,9 @@ namespace Polygon_mesh_processing * provides a set of instructions for isotropic remeshing to achieve variable * mesh edge lengths as a function of local discrete curvatures. * +* The local discrete curvatures are calculated using the +* `CGAL::Polygon_mesh_processing::interpolated_corrected_principal_curvatures_and_directions()` function. +* * Edges longer than the local target edge length are split in half, while * edges shorter than the local target edge length are collapsed. * From 7037103416379b20e7254325460696b95d98a20d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sat, 9 Sep 2023 12:49:03 +0200 Subject: [PATCH 072/124] Introduce ball_radius NP for curvature calculation in Adaptive_sizing_field --- .../Adaptive_sizing_field.h | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 60c9e5f76597..783b7f5e5669 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -71,31 +71,50 @@ class Adaptive_sizing_field : public Sizing_field_base * * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, * model of `Range`. Its iterator type is `ForwardIterator`. + * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * @param tol the error tolerance, used together with curvature to derive target edge length. - * Lower tolerance values will result in shorter mesh edges. + * Lower tolerance values will result in shorter mesh edges. * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed * edge length. * @param face_range the range of triangular faces defining one or several surface patches * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the * same mesh as the one used in `isotropic_remeshing()`. + * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below + + * \cgalNamedParamsBegin + * \cgalParamNBegin{ball_radius} + * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures + * by summing measures of faces inside a ball of this radius centered at the + * vertex expanded from. The summed face measures are weighted by their + * inclusion ratio inside this ball.} + * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. + * It can effectively smooth the curvature field and consequently the sizing field.} + * \cgalParamType{`Base::FT`} + * \cgalParamDefault{`-1`} + * \cgalParamExtra{If this parameter is omitted (`-1`), the expansion will just by a weightless sum of + * measures on faces around the vertex.} + * \cgalParamNEnd */ - template + template Adaptive_sizing_field(const double tol , const std::pair& edge_len_min_max , const FaceRange& face_range - , PolygonMesh& pmesh) + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) : tol(tol) , m_short(edge_len_min_max.first) , m_long(edge_len_min_max.second) , m_vpmap(get(CGAL::vertex_point, pmesh)) , m_vertex_sizing_map(get(Vertex_property_tag(), pmesh)) { + if (face_range.size() == faces(pmesh).size()) { // calculate curvature from the whole mesh - calc_sizing_map(pmesh); + calc_sizing_map(pmesh, np); } else { @@ -108,14 +127,16 @@ class Adaptive_sizing_field : public Sizing_field_base is_selected, std::back_inserter(selection)); Face_filtered_graph ffg(pmesh, selection); - calc_sizing_map(ffg); + calc_sizing_map(ffg, np); } } ///@} private: - template - void calc_sizing_map(FaceGraph& face_graph) + template + void calc_sizing_map(FaceGraph& face_graph + , const NamedParameters& np) { //todo ip: please check if this is good enough to store curvature typedef Principal_curvatures_and_directions Principal_curvatures; @@ -123,6 +144,10 @@ class Adaptive_sizing_field : public Sizing_field_base typedef typename boost::property_map::type Vertex_curvature_map; + using parameters::choose_parameter; + using parameters::get_parameter; + typename Base::FT radius = choose_parameter(get_parameter(np, internal_np::ball_radius), -1); + #ifdef CGAL_PMP_REMESHING_VERBOSE int oversize = 0; int undersize = 0; @@ -132,7 +157,8 @@ class Adaptive_sizing_field : public Sizing_field_base Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), face_graph); interpolated_corrected_principal_curvatures_and_directions(face_graph - , vertex_curvature_map); + , vertex_curvature_map + , parameters::ball_radius(radius)); // calculate vertex sizing field L(x_i) from the curvature field for(vertex_descriptor v : vertices(face_graph)) { From 426c6f9f5b156b1938ab59f19e2682c5cf7e3ff5 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Sat, 9 Sep 2023 15:56:04 +0200 Subject: [PATCH 073/124] Update polyhedron demo with ball_radius np in Adaptive_sizing_field --- .../Adaptive_sizing_field.h | 7 +- .../Plugins/PMP/Isotropic_remeshing_dialog.ui | 219 ++++++++++-------- .../PMP/Isotropic_remeshing_plugin.cpp | 73 ++++-- 3 files changed, 191 insertions(+), 108 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 783b7f5e5669..6525d7b2f85f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -85,10 +85,9 @@ class Adaptive_sizing_field : public Sizing_field_base * \cgalNamedParamsBegin * \cgalParamNBegin{ball_radius} - * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures - * by summing measures of faces inside a ball of this radius centered at the - * vertex expanded from. The summed face measures are weighted by their - * inclusion ratio inside this ball.} + * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. + * It can potentially smooth the curvature and consequently the sizing field in the + * case of noisy input.} * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. * It can effectively smooth the curvature field and consequently the sizing field.} * \cgalParamType{`Base::FT`} diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui index 540a52c1bff9..fd2e9c6ab6c3 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_dialog.ui @@ -10,7 +10,7 @@ 0 0 381 - 545 + 620 @@ -59,7 +59,68 @@ Isotropic remeshing - + + + + + + + + + + + + Minimum edge length + + + + + + + + + + + + + + 0 + + + 100 + + + 1 + + + + + + + Error tolerance + + + + + + + + + + true + + + + + + + + + + 0.00 + + + @@ -109,7 +170,7 @@ - + Qt::Vertical @@ -122,87 +183,17 @@ - - - - Qt::Vertical - - - QSizePolicy::Maximum - - - - 20 - 24 - - - - - - - - Error tolerance - - - - - - - - - - 0.00 - - - - - - - Minimum edge length - - - - - - - - - - - - - - 0 - - - 100 - - - 1 - - - - - - - - - - true - - - - - + + - Number of Smoothing iterations + Allow 1D smoothing along borders Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + Protect borders/selected edges @@ -212,24 +203,14 @@ - + - - - - Allow 1D smoothing along borders - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + Preserve duplicated edges @@ -239,7 +220,7 @@ - + @@ -252,7 +233,7 @@ - + Number of Main iterations @@ -265,6 +246,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 20 + 24 + + + + @@ -312,6 +309,46 @@ + + + + Number of Smoothing iterations + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Ball radius + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Curvature smoothing + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + -1 + + + false + + + diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index a819c2c22943..06155a62c217 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -300,7 +300,8 @@ class Polyhedron_demo_isotropic_remeshing_plugin : const double target_length, const double error_tol, const double min_length, - const double max_length) + const double max_length, + const double curv_ball_r) { std::vector p_edges; for(edge_descriptor e : edges(pmesh)) @@ -335,7 +336,8 @@ class Polyhedron_demo_isotropic_remeshing_plugin : error_tol , edge_min_max , faces(*selection_item->polyhedron()) - , *selection_item->polyhedron()); + , *selection_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r)); CGAL::Polygon_mesh_processing::split_long_edges( p_edges , adaptive_sizing @@ -394,6 +396,8 @@ public Q_SLOTS: unsigned int nb_smooth = ui.nbSmoothing_spinbox->value(); bool protect = ui.protect_checkbox->isChecked(); bool smooth_features = ui.smooth1D_checkbox->isChecked(); + bool curv_smooth = ui.curvSmooth_checkbox->isChecked(); + double curv_ball_r = ui.curvSmoothBallR_edit->value(); // wait cursor QApplication::setOverrideCursor(Qt::WaitCursor); @@ -425,7 +429,8 @@ public Q_SLOTS: { if (edges_only) { - do_split_edges(edge_sizing_type, selection_item, pmesh, target_length, error_tol, min_length, max_length); + do_split_edges(edge_sizing_type, selection_item, pmesh, + target_length, error_tol, min_length, max_length, curv_ball_r); } else //not edges_only { @@ -461,7 +466,8 @@ public Q_SLOTS: } else { - do_split_edges(edge_sizing_type, selection_item, pmesh, target_length, error_tol, min_length, max_length); + do_split_edges(edge_sizing_type, selection_item, pmesh, + target_length, error_tol, min_length, max_length, curv_ball_r); } } @@ -517,7 +523,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*selection_item->polyhedron()) - , *selection_item->polyhedron()); + , *selection_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r)); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing(faces(*selection_item->polyhedron()) , adaptive_sizing_field @@ -600,7 +607,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*selection_item->polyhedron()) - , *selection_item->polyhedron()); + , *selection_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r)); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing(selection_item->selected_facets , adaptive_sizing_field @@ -675,9 +683,10 @@ public Q_SLOTS: { std::pair edge_min_max{min_length, max_length}; PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol - , edge_min_max - , faces(pmesh) - , pmesh); + , edge_min_max + , faces(pmesh) + , pmesh + , CGAL::parameters::ball_radius(curv_ball_r)); CGAL::Polygon_mesh_processing::split_long_edges( edges_to_split , target_length @@ -704,7 +713,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(pmesh) - , pmesh); + , pmesh + , CGAL::parameters::ball_radius(curv_ball_r)); CGAL::Polygon_mesh_processing::split_long_edges( edges_to_split , adaptive_sizing_field @@ -799,7 +809,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol , edge_min_max , faces(*poly_item->polyhedron()) - , *poly_item->polyhedron()); + , *poly_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r)); if (fpmap_valid) CGAL::Polygon_mesh_processing::isotropic_remeshing( faces(*poly_item->polyhedron()) @@ -862,6 +873,8 @@ public Q_SLOTS: unsigned int nb_iter = 1; bool protect = false; bool smooth_features = true; + bool curv_smooth = false; + double curv_ball_r = -1; std::vector selection; for(int index : scene->selectionIndices()) @@ -902,6 +915,8 @@ public Q_SLOTS: nb_iter = ui.nbIterations_spinbox->value(); protect = ui.protect_checkbox->isChecked(); smooth_features = ui.smooth1D_checkbox->isChecked(); + curv_smooth = ui.curvSmooth_checkbox->isChecked(); + curv_ball_r = ui.curvSmoothBallR_edit->value(); } } } @@ -1011,6 +1026,8 @@ public Q_SLOTS: unsigned int nb_iter_; bool protect_; bool smooth_features_; + bool curv_smooth_; + double curv_ball_r_; protected: void remesh(Scene_facegraph_item* poly_item, @@ -1042,7 +1059,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ , edge_min_max , faces(*poly_item->polyhedron()) - , *poly_item->polyhedron()); + , *poly_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r_)); CGAL::Polygon_mesh_processing::split_long_edges( border_edges , target_length_ @@ -1072,7 +1090,8 @@ public Q_SLOTS: PMP::Adaptive_sizing_field adaptive_sizing_field(error_tol_ , edge_min_max , faces(*poly_item->polyhedron()) - , *poly_item->polyhedron()); + , *poly_item->polyhedron() + , CGAL::parameters::ball_radius(curv_ball_r_)); CGAL::Polygon_mesh_processing::isotropic_remeshing( faces(*poly_item->polyhedron()) , target_length_ @@ -1234,6 +1253,10 @@ public Q_SLOTS: ui.minEdgeLength_edit->hide(); ui.maxEdgeLength_label->hide(); ui.maxEdgeLength_edit->hide(); + ui.curvSmooth_checkbox->hide(); + ui.curvSmooth_label->hide(); + ui.curvSmoothBallR_edit->hide(); + ui.curvSmoothBallR_label->hide(); } else if (index == 1) { @@ -1245,6 +1268,25 @@ public Q_SLOTS: ui.minEdgeLength_edit->show(); ui.maxEdgeLength_label->show(); ui.maxEdgeLength_edit->show(); + ui.curvSmooth_checkbox->show(); + ui.curvSmooth_label->show(); + ui.curvSmoothBallR_edit->show(); + ui.curvSmoothBallR_label->show(); + } + } + + void update_after_curvSmooth_click() + { + if (ui.curvSmooth_checkbox->isChecked()) + { + ui.curvSmoothBallR_label->setEnabled(true); + ui.curvSmoothBallR_edit->setEnabled(true); + } + else + { + ui.curvSmoothBallR_label->setEnabled(false); + ui.curvSmoothBallR_edit->setValue(-1); + ui.curvSmoothBallR_edit->setEnabled(false); } } @@ -1268,6 +1310,7 @@ public Q_SLOTS: connect(ui.splitEdgesOnly_checkbox, SIGNAL(clicked(bool)), this, SLOT(update_after_splitEdgesOnly_click())); connect(ui.edgeSizing_type_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(on_edgeSizing_type_combo_box_changed(int))); + connect(ui.curvSmooth_checkbox, SIGNAL(clicked(bool)), this, SLOT(update_after_curvSmooth_click())); //Set default parameters Scene_interface::Bbox bbox = poly_item != nullptr ? poly_item->bbox() @@ -1313,6 +1356,10 @@ public Q_SLOTS: on_edgeSizing_type_combo_box_changed(0); ui.protect_checkbox->setChecked(false); ui.smooth1D_checkbox->setChecked(true); + ui.curvSmooth_checkbox->setChecked(false); + ui.curvSmoothBallR_label->setEnabled(false); + ui.curvSmoothBallR_edit->setEnabled(false); + ui.curvSmoothBallR_edit->setValue(-1); if (nullptr != selection_item) { From b370381e0a54153a2c48b8748001b244c4d232d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 12 Sep 2023 15:26:31 +0200 Subject: [PATCH 074/124] add missing ending --- .../include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 6525d7b2f85f..d77803bba822 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -95,6 +95,7 @@ class Adaptive_sizing_field : public Sizing_field_base * \cgalParamExtra{If this parameter is omitted (`-1`), the expansion will just by a weightless sum of * measures on faces around the vertex.} * \cgalParamNEnd + * \cgalNamedParamsEnd */ template From 04be232d145595b49e83bbda302ebbc2fbc493bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 12 Sep 2023 15:29:55 +0200 Subject: [PATCH 075/124] update to macro update --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../Isotropic_remeshing/Sizing_field_base.h | 2 +- .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index d77803bba822..ed7332e13143 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -38,7 +38,7 @@ namespace Polygon_mesh_processing * Edges longer than the local target edge length are split in half, while * edges shorter than the local target edge length are collapsed. * -* \cgalModels PMPSizingField +* \cgalModels{PMPSizingField} * * \sa `isotropic_remeshing` * \sa `Uniform_sizing_field` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h index 580601bbaecb..5278896f9d83 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h @@ -29,7 +29,7 @@ namespace Polygon_mesh_processing * pure virtual class serving as a base for sizing field classes utilized in isotropic * remeshing. * -* \cgalModels PMPSizingField +* \cgalModels{PMPSizingField} * * \sa `isotropic_remeshing()` * \sa `Uniform_sizing_field` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 02d21df369be..b454b5d2abf1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -31,7 +31,7 @@ namespace Polygon_mesh_processing * Edges longer than the 4/3 of the target edge length will be split in half, while * edges shorter than the 4/5 of the target edge length will be collapsed. * -* \cgalModels PMPSizingField +* \cgalModels{PMPSizingField} * * \sa `isotropic_remeshing` * \sa `Adaptive_sizing_field` From 42c02d9e34e52131f3f08da840afc24b355721e7 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 12 Sep 2023 20:13:13 -0600 Subject: [PATCH 076/124] Add constructor with VPMap input in Adaptive_sizing_field --- .../Adaptive_sizing_field.h | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index ed7332e13143..7b29f3faadf1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -46,8 +46,12 @@ namespace Polygon_mesh_processing * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. */ -template -class Adaptive_sizing_field : public Sizing_field_base +template ::const_type> +class Adaptive_sizing_field +#ifndef DOXYGEN_RUNNING + : public Sizing_field_base +#endif { private: typedef Sizing_field_base Base; @@ -62,7 +66,6 @@ class Adaptive_sizing_field : public Sizing_field_base typedef typename Base::face_descriptor face_descriptor; typedef typename Base::halfedge_descriptor halfedge_descriptor; typedef typename Base::vertex_descriptor vertex_descriptor; - typedef typename Base::DefaultVPMap DefaultVPMap; /// \name Creation /// @{ @@ -79,6 +82,8 @@ class Adaptive_sizing_field : public Sizing_field_base * edge length. * @param face_range the range of triangular faces defining one or several surface patches * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. + * \param vpmap is the input vertex point map that associates points to the vertices of + * an input mesh. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the * same mesh as the one used in `isotropic_remeshing()`. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below @@ -102,15 +107,15 @@ class Adaptive_sizing_field : public Sizing_field_base Adaptive_sizing_field(const double tol , const std::pair& edge_len_min_max , const FaceRange& face_range + , const VPMap& vpmap , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) : tol(tol) , m_short(edge_len_min_max.first) , m_long(edge_len_min_max.second) - , m_vpmap(get(CGAL::vertex_point, pmesh)) + , m_vpmap(vpmap) , m_vertex_sizing_map(get(Vertex_property_tag(), pmesh)) { - if (face_range.size() == faces(pmesh).size()) { // calculate curvature from the whole mesh @@ -130,6 +135,51 @@ class Adaptive_sizing_field : public Sizing_field_base calc_sizing_map(ffg, np); } } + + /*! + * returns an object to serve as criteria for adaptive curvature-based edge lengths. It calls + * the first constructor using default values for the vertex point map of the input polygon mesh. + * + * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, + * model of `Range`. Its iterator type is `ForwardIterator`. + * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" + * + * @param tol the error tolerance, used together with curvature to derive target edge length. + * Lower tolerance values will result in shorter mesh edges. + * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed + * edge length. + * @param face_range the range of triangular faces defining one or several surface patches + * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. + * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the + * same mesh as the one used in `isotropic_remeshing()`. Additionally, the default vertex point + * map of pmesh is used to construct the class. + * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below + + * \cgalNamedParamsBegin + * \cgalParamNBegin{ball_radius} + * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. + * It can potentially smooth the curvature and consequently the sizing field in the + * case of noisy input.} + * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. + * It can effectively smooth the curvature field and consequently the sizing field.} + * \cgalParamType{`Base::FT`} + * \cgalParamDefault{`-1`} + * \cgalParamExtra{If this parameter is omitted (`-1`), the expansion will just by a weightless sum of + * measures on faces around the vertex.} + * \cgalParamNEnd + * \cgalNamedParamsEnd + */ + template + Adaptive_sizing_field(const double tol + , const std::pair& edge_len_min_max + , const FaceRange& face_range + , PolygonMesh& pmesh + , const NamedParameters& np = parameters::default_values()) + : Adaptive_sizing_field(tol, edge_len_min_max, face_range, + get(CGAL::vertex_point, pmesh), pmesh, np) + {} + ///@} private: @@ -277,7 +327,7 @@ class Adaptive_sizing_field : public Sizing_field_base const FT tol; const FT m_short; const FT m_long; - const DefaultVPMap m_vpmap; + const VPMap m_vpmap; VertexSizingMap m_vertex_sizing_map; }; From 4d06df0622dae88f291a0136c11642599216f0de Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 12 Sep 2023 21:26:14 -0600 Subject: [PATCH 077/124] Move Sizing_field_base one directory down --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../{Isotropic_remeshing => }/Sizing_field_base.h | 3 --- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{Isotropic_remeshing => }/Sizing_field_base.h (96%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 7b29f3faadf1..7bb8cbe8de4b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include #include diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h similarity index 96% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h index 5278896f9d83..8cb12191ad1a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Isotropic_remeshing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h @@ -49,9 +49,6 @@ class Sizing_field_base typedef PolygonMesh PM; typedef typename boost::property_traits::value_type Point; -protected: - typedef typename boost::property_map::const_type DefaultVPMap; - public: typedef typename CGAL::Kernel_traits::Kernel K; typedef typename boost::graph_traits::face_descriptor face_descriptor; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index b454b5d2abf1..5b3090f7945f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include From 12cc789f33a3539568724001f3d81eb9686306de Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 12 Sep 2023 21:48:13 -0600 Subject: [PATCH 078/124] Remove temp todos --- .../internal/Isotropic_remeshing/remesh_impl.h | 5 ----- .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) 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 7453a9482edc..aae916ff0953 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 @@ -55,9 +55,6 @@ #include #include -//todo ip: temp -#define CGAL_PMP_REMESHING_VERBOSE - #ifdef CGAL_PMP_REMESHING_DEBUG #include #define CGAL_DUMP_REMESHING_STEPS @@ -1120,7 +1117,6 @@ namespace internal { Point proj = trees[patch_id_to_index_map[get_patch_id(face(halfedge(v, mesh_), mesh_))]]->closest_point(get(vpmap_, v)); put(vpmap_, v, proj); - //todo ip - also update sizing field here? } CGAL_assertion(!input_mesh_is_valid_ || is_valid_polygon_mesh(mesh_)); #ifdef CGAL_PMP_REMESHING_DEBUG @@ -1149,7 +1145,6 @@ namespace internal { continue; //note if v is constrained, it has not moved put(vpmap_, v, proj(v)); - //todo ip: also update sizing field here? } CGAL_assertion(is_valid(mesh_)); #ifdef CGAL_PMP_REMESHING_DEBUG diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 53084cce5e02..7911c3340c56 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -564,7 +564,7 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, barycenters.emplace_back(v, vn, get(vpm, v) + move); } - else //todo ip: do border edges need to be handled with the sizing field? + else { if (!relax_constraints) continue; Vector_3 vn(NULL_VECTOR); From 0481c624e2ff2ca626bec9a17f6c815ba54c72f7 Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Tue, 12 Sep 2023 23:26:34 -0600 Subject: [PATCH 079/124] Cleanup the example --- .../isotropic_remeshing_with_sizing_example.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index dacc3e02e1b2..5ecc651f0966 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -13,10 +13,7 @@ namespace PMP = CGAL::Polygon_mesh_processing; int main(int argc, char* argv[]) { -// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/lion-head.off"); -// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/hand.off"); const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/nefertiti.off"); -// const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube.off"); Mesh mesh; if (!PMP::IO::read_polygon_mesh(filename, mesh) || !CGAL::is_triangle_mesh(mesh)) { @@ -37,6 +34,7 @@ int main(int argc, char* argv[]) sizing_field, mesh, PMP::parameters::number_of_iterations(nb_iter) + .number_of_relaxation_steps(3) ); CGAL::IO::write_polygon_mesh("out.off", mesh, CGAL::parameters::stream_precision(17)); From 230de52aaf61647db041f7911a1396638ee1a9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 14 Sep 2023 15:46:21 +0200 Subject: [PATCH 080/124] remove unused variables --- .../internal/Isotropic_remeshing/remesh_impl.h | 2 -- 1 file changed, 2 deletions(-) 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 aae916ff0953..07170bd81777 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 @@ -415,7 +415,6 @@ namespace internal { //the edge with longest length auto eit = long_edges.begin(); halfedge_descriptor he = eit->first; - double sqlen = eit->second; long_edges.erase(eit); //split edge @@ -515,7 +514,6 @@ namespace internal { //the edge with longest length auto eit = long_edges.begin(); halfedge_descriptor he = eit->first; - double sqlen = eit->second; long_edges.erase(eit); #ifdef CGAL_PMP_REMESHING_VERBOSE_PROGRESS From 1d21d57f27e397baa6f936fb17d5bf8bacd99db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 14 Sep 2023 15:49:28 +0200 Subject: [PATCH 081/124] remove no longer used parameter --- .../internal/Isotropic_remeshing/remesh_impl.h | 1 - .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 07170bd81777..cc80c0534fc3 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 @@ -235,7 +235,6 @@ namespace internal { typename SizingFunction> bool constraints_are_short_enough(const PM& pmesh, EdgeConstraintMap ecmap, - VertexPointMap vpmap, const FacePatchMap& fpm, const SizingFunction& sizing) { 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 45ce27304cb8..6a04386aa10c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -274,7 +274,7 @@ void isotropic_remeshing(const FaceRange& faces msg.append(" true with constraints larger than 4/3 * target_edge_length."); msg.append(" Remeshing aborted."); CGAL_precondition_msg( - internal::constraints_are_short_enough(pmesh, ecmap, vpmap, fpmap, sizing), + internal::constraints_are_short_enough(pmesh, ecmap, fpmap, sizing), msg.c_str()); } #endif From 324f1331256eee6bdb62815a2ca74a9f6216188a Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 14 Sep 2023 10:09:12 -0600 Subject: [PATCH 082/124] Update removed arguments --- .../internal/Isotropic_remeshing/remesh_impl.h | 1 - .../demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 2 -- 2 files changed, 3 deletions(-) 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 cc80c0534fc3..f7d93ab1e307 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 @@ -230,7 +230,6 @@ namespace internal { template bool constraints_are_short_enough(const PM& pmesh, diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 06155a62c217..02c61ba52c2e 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -438,7 +438,6 @@ public Q_SLOTS: !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( *selection_item->polyhedron(), selection_item->constrained_edges_pmap(), - get(CGAL::vertex_point, *selection_item->polyhedron()), CGAL::Constant_property_map(1), CGAL::Polygon_mesh_processing::Uniform_sizing_field( 4. / 3. * target_length, pmesh))) { @@ -749,7 +748,6 @@ public Q_SLOTS: !CGAL::Polygon_mesh_processing::internal::constraints_are_short_enough( pmesh, ecm, - get(CGAL::vertex_point, pmesh), CGAL::Constant_property_map(1), CGAL::Polygon_mesh_processing::Uniform_sizing_field(4. / 3. * target_length, pmesh))) { From 7128ef9ea6cc2c453ecd0cc28871be8c15fef41d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Thu, 14 Sep 2023 10:25:10 -0600 Subject: [PATCH 083/124] Add STL 'limits' include to address the failing test --- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index 69b2afaaca01..68f77d9f6c82 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -5,6 +5,7 @@ #include #include +#include typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef K::Point_3 Point_3; From 28ac57e75120b8ccf87a6a2e24432569597c158d Mon Sep 17 00:00:00 2001 From: Ivan Paden Date: Fri, 15 Sep 2023 09:22:15 -0600 Subject: [PATCH 084/124] Fix for a failing MVSC test --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 7bb8cbe8de4b..705031cef3a7 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -213,8 +213,8 @@ class Adaptive_sizing_field for(vertex_descriptor v : vertices(face_graph)) { auto vertex_curv = get(vertex_curvature_map, v); - const FT max_absolute_curv = CGAL::max(CGAL::abs(vertex_curv.max_curvature) - , CGAL::abs(vertex_curv.min_curvature)); + const FT max_absolute_curv = (CGAL::max)(CGAL::abs(vertex_curv.max_curvature) + , CGAL::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); if (vertex_size_sq > CGAL::square(m_long)) { From 959311048bb31befd233fd56e8b42fab483a11c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 21 Sep 2023 00:40:33 -0600 Subject: [PATCH 085/124] Fix Adaptive_sizing_field template issue --- .../isotropic_remeshing_with_sizing_example.cpp | 2 +- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 5ecc651f0966..1fc3c4960d38 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -26,7 +26,7 @@ int main(int argc, char* argv[]) const double tol = 0.001; const std::pair edge_min_max{0.001, 0.5}; - PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, faces(mesh), mesh); + PMP::Adaptive_sizing_field sizing_field(tol, edge_min_max, faces(mesh), mesh); unsigned int nb_iter = 5; PMP::isotropic_remeshing( diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 705031cef3a7..10c563072977 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -47,14 +47,14 @@ namespace Polygon_mesh_processing * has an internal property map for `CGAL::vertex_point_t`. */ template ::const_type> + class VPMap = typename boost::property_map::const_type> class Adaptive_sizing_field #ifndef DOXYGEN_RUNNING : public Sizing_field_base #endif { private: - typedef Sizing_field_base Base; + typedef Sizing_field_base Base; typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map::type VertexSizingMap; From 7cb3a58185d653ac450c4f2a1e547849409814eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 22 Sep 2023 07:15:19 -0600 Subject: [PATCH 086/124] Update Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp Co-authored-by: Andreas Fabri --- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index 68f77d9f6c82..f07d4c027f47 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -43,7 +43,7 @@ int main(int argc, char* argv[]) * More information on quality metrics can be found here: https://ieeexplore.ieee.org/document/9167456 */ std::cout << "Remeshing done, checking triangle quality...\n" << std::endl; - double qmin = std::numeric_limits::max(); // minimum triangle quality + double qmin = (std::numeric_limits::max)(); // minimum triangle quality double qavg = 0.; // average quality double min_angle = std::numeric_limits::max(); // minimum angle double avg_min_angle = 0.; // average minimum angle From 20a735cc599e81027328482022efa0300a6c686c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 22 Sep 2023 07:15:38 -0600 Subject: [PATCH 087/124] Update Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp Co-authored-by: Andreas Fabri --- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index f07d4c027f47..a6216663596f 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -45,7 +45,7 @@ int main(int argc, char* argv[]) std::cout << "Remeshing done, checking triangle quality...\n" << std::endl; double qmin = (std::numeric_limits::max)(); // minimum triangle quality double qavg = 0.; // average quality - double min_angle = std::numeric_limits::max(); // minimum angle + double min_angle = (std::numeric_limits::max)(); // minimum angle double avg_min_angle = 0.; // average minimum angle for (auto face : mesh.faces()) { From 64af00b2ad4c71680ebd559d923bed8625212cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:32:32 -0600 Subject: [PATCH 088/124] Fix formatting --- .../Adaptive_sizing_field.h | 34 +++++++++---------- .../Uniform_sizing_field.h | 3 +- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 10c563072977..b465627b6b9f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -206,15 +206,15 @@ class Adaptive_sizing_field #endif Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), face_graph); - interpolated_corrected_principal_curvatures_and_directions(face_graph - , vertex_curvature_map - , parameters::ball_radius(radius)); + interpolated_corrected_principal_curvatures_and_directions(face_graph, + vertex_curvature_map, + parameters::ball_radius(radius)); // calculate vertex sizing field L(x_i) from the curvature field for(vertex_descriptor v : vertices(face_graph)) { auto vertex_curv = get(vertex_curvature_map, v); - const FT max_absolute_curv = (CGAL::max)(CGAL::abs(vertex_curv.max_curvature) - , CGAL::abs(vertex_curv.min_curvature)); + const FT max_absolute_curv = (CGAL::max)(CGAL::abs(vertex_curv.max_curvature), + CGAL::abs(vertex_curv.min_curvature)); const FT vertex_size_sq = 6 * tol / max_absolute_curv - 3 * CGAL::square(tol); if (vertex_size_sq > CGAL::square(m_long)) { @@ -257,16 +257,17 @@ class Adaptive_sizing_field } public: - FT get_sizing(const vertex_descriptor v) const { - CGAL_assertion(get(m_vertex_sizing_map, v)); - return get(m_vertex_sizing_map, v); - } + FT get_sizing(const vertex_descriptor v) const + { + CGAL_assertion(get(m_vertex_sizing_map, v)); + return get(m_vertex_sizing_map, v); + } std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); - FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), - get(m_vertex_sizing_map, target(h, pmesh)))); + FT sqtarg_len = CGAL::square(4./3. * (CGAL::min)(get(m_vertex_sizing_map, source(h, pmesh)), + get(m_vertex_sizing_map, target(h, pmesh)))); CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); if(sqlen > sqtarg_len) @@ -275,12 +276,11 @@ class Adaptive_sizing_field return std::nullopt; } - std::optional is_too_long(const vertex_descriptor va, - const vertex_descriptor vb) const + std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); - FT sqtarg_len = CGAL::square(4./3. * CGAL::min(get(m_vertex_sizing_map, va), - get(m_vertex_sizing_map, vb))); + FT sqtarg_len = CGAL::square(4./3. * (CGAL::min)(get(m_vertex_sizing_map, va), + get(m_vertex_sizing_map, vb))); CGAL_assertion(get(m_vertex_sizing_map, va)); CGAL_assertion(get(m_vertex_sizing_map, vb)); if (sqlen > sqtarg_len) @@ -292,8 +292,8 @@ class Adaptive_sizing_field std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const { const FT sqlen = sqlength(h, pmesh); - FT sqtarg_len = CGAL::square(4./5. * CGAL::min(get(m_vertex_sizing_map, source(h, pmesh)), - get(m_vertex_sizing_map, target(h, pmesh)))); + FT sqtarg_len = CGAL::square(4./5. * (CGAL::min)(get(m_vertex_sizing_map, source(h, pmesh)), + get(m_vertex_sizing_map, target(h, pmesh)))); CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); if (sqlen < sqtarg_len) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 5b3090f7945f..fc4540ab59bd 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -111,8 +111,7 @@ class Uniform_sizing_field return std::nullopt; } - std::optional is_too_long(const vertex_descriptor va, - const vertex_descriptor vb) const + std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); if (sqlen > m_sq_long) From 896e4913ff92504403a0de0d33408f5737d78d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Sat, 23 Sep 2023 21:58:31 -0600 Subject: [PATCH 089/124] Update authors --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index b465627b6b9f..b17b92088f6c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -1,4 +1,4 @@ -// Copyright (c) 2020 GeometryFactory (France) +// Copyright (c) 2023 GeometryFactory (France) // All rights reserved. // // This file is part of CGAL (www.cgal.org) @@ -8,7 +8,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial // // -// Author(s) : Jane Tournois +// Author(s) : Ivan Paden #ifndef CGAL_PMP_REMESHING_ADAPTIVE_SIZING_FIELD_H #define CGAL_PMP_REMESHING_ADAPTIVE_SIZING_FIELD_H diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 7911c3340c56..ab89f75afe34 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 GeometryFactory (France). +// Copyright (c) 2015-2023 GeometryFactory (France). // All rights reserved. // // This file is part of CGAL (www.cgal.org). @@ -8,7 +8,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-Commercial // // -// Author(s) : Jane Tournois +// Author(s) : Jane Tournois, Ivan Paden #ifndef CGAL_POLYGON_MESH_PROCESSING_TANGENTIAL_RELAXATION_H From 65c75c5d43d4dc540d0b8adbf826a3b9ecead521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:45:03 -0600 Subject: [PATCH 090/124] Update data type --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index b17b92088f6c..20ec7162021c 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -104,7 +104,7 @@ class Adaptive_sizing_field */ template - Adaptive_sizing_field(const double tol + Adaptive_sizing_field(const FT tol , const std::pair& edge_len_min_max , const FaceRange& face_range , const VPMap& vpmap @@ -171,7 +171,7 @@ class Adaptive_sizing_field */ template - Adaptive_sizing_field(const double tol + Adaptive_sizing_field(const FT tol , const std::pair& edge_len_min_max , const FaceRange& face_range , PolygonMesh& pmesh From 1e1eb19a5bdedc4833c1a1d8920b2e4f7840a6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:28:57 -0600 Subject: [PATCH 091/124] Update Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h Co-authored-by: Andreas Fabri --- .../include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index fc4540ab59bd..e7f26db5d971 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -33,7 +33,7 @@ namespace Polygon_mesh_processing * * \cgalModels{PMPSizingField} * -* \sa `isotropic_remeshing` +* \sa `isotropic_remeshing()` * \sa `Adaptive_sizing_field` * * @tparam PolygonMesh model of `MutableFaceGraph` that From 99f8120e5b2e5ad0e7bb782d8ed9028c19b4d164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:29:08 -0600 Subject: [PATCH 092/124] Update Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h Co-authored-by: Andreas Fabri --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 20ec7162021c..b4f7c6fc8c45 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -40,7 +40,7 @@ namespace Polygon_mesh_processing * * \cgalModels{PMPSizingField} * -* \sa `isotropic_remeshing` +* \sa `isotropic_remeshing()` * \sa `Uniform_sizing_field` * * @tparam PolygonMesh model of `MutableFaceGraph` that From bde55d8e4c825c24cecb7ce9913d3b3da1414193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Wed, 27 Sep 2023 21:17:07 -0600 Subject: [PATCH 093/124] Update docs --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index b4f7c6fc8c45..82a89b144dc5 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -29,8 +29,8 @@ namespace Polygon_mesh_processing { /*! * \ingroup PMP_meshing_grp -* provides a set of instructions for isotropic remeshing to achieve variable -* mesh edge lengths as a function of local discrete curvatures. +* provides criteria for isotropic remeshing to achieve variable mesh edge lengths as a function +* of local discrete curvatures. * * The local discrete curvatures are calculated using the * `CGAL::Polygon_mesh_processing::interpolated_corrected_principal_curvatures_and_directions()` function. diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index e7f26db5d971..e2e655aa1475 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -25,8 +25,7 @@ namespace Polygon_mesh_processing { /*! * \ingroup PMP_meshing_grp -* provides a set of instructions for isotropic remeshing to achieve uniform -* mesh edge lengths. +* provides criteria for isotropic remeshing to achieve uniform mesh edge lengths. * * Edges longer than the 4/3 of the target edge length will be split in half, while * edges shorter than the 4/5 of the target edge length will be collapsed. From e49789b3d58097b226e1d00fbe06f99afd4881eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:44:47 -0600 Subject: [PATCH 094/124] Update Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h Co-authored-by: Jane Tournois --- .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6a04386aa10c..41b4301ca49a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -50,7 +50,7 @@ namespace Polygon_mesh_processing { * * @param pmesh a polygon mesh with triangulated surface patches to be remeshed * @param faces the range of triangular faces defining one or several surface patches to be remeshed -* @param sizing uniform or adaptive sizing field that determines a target length for individual edges. +* @param sizing field that determines a target length for individual edges. * If a number convertible to a double is passed, it will use a `Uniform_sizing_field()` * with the number as a target edge length. * If `0` is passed then only the edge-flip, tangential relaxation, and projection steps will be done. From 21f6580d64154aeab2f95d38a0163ef6ae67ae6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 20:50:58 -0600 Subject: [PATCH 095/124] Update Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h Co-authored-by: Jane Tournois --- .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 41b4301ca49a..43bb5605a27b 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -51,7 +51,7 @@ namespace Polygon_mesh_processing { * @param pmesh a polygon mesh with triangulated surface patches to be remeshed * @param faces the range of triangular faces defining one or several surface patches to be remeshed * @param sizing field that determines a target length for individual edges. -* If a number convertible to a double is passed, it will use a `Uniform_sizing_field()` +* If a number convertible to a `double` is passed, `Uniform_sizing_field()` will be used, * with the number as a target edge length. * If `0` is passed then only the edge-flip, tangential relaxation, and projection steps will be done. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below From 7b7dfa2e0a3520b34228e9ac869e78dff3eb4966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 20:51:28 -0600 Subject: [PATCH 096/124] Update Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h Co-authored-by: Jane Tournois --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index afd1db889147..a359f0f64591 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -12,7 +12,7 @@ class PMPSizingField{ /// @{ /// Vertex descriptor type -typedef unspecified_type vertex_descriptor; +typedef boost::graph_traits::vertex_descriptor vertex_descriptor; /// Halfedge descriptor type typedef unspecified_type halfedge_descriptor; From 178d967d4bae4d69e2add3a14bf1415f5108ac3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 20:58:01 -0600 Subject: [PATCH 097/124] Apply suggestions from code review Co-authored-by: Jane Tournois --- .../Concepts/PMPSizingField.h | 21 ++++++++-------- .../PackageDescription.txt | 2 +- .../Polygon_mesh_processing.txt | 11 +++++---- .../Adaptive_sizing_field.h | 24 +++++++------------ .../Uniform_sizing_field.h | 17 +++++++------ 5 files changed, 33 insertions(+), 42 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index a359f0f64591..c9e66b1d2745 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -15,15 +15,15 @@ class PMPSizingField{ typedef boost::graph_traits::vertex_descriptor vertex_descriptor; /// Halfedge descriptor type -typedef unspecified_type halfedge_descriptor; +typedef boost::graph_traits::halfedge_descriptor halfedge_descriptor; -/// 3D point type +/// 3D point type matching the value type of the vertex property map passed to `CGAL::Polygon_mesh_processing::isotropic_remeshing()` typedef unspecified_type Point_3; -/// Polygon mesh type +/// Polygon mesh type matching the type passed to `CGAL::Polygon_mesh_processing::isotropic_remeshing()` typedef unspecified_type PolygonMesh; -/// Numerical type +/// Number type matching the `FT` type of the geometric traits passed to `CGAL::Polygon_mesh_processing::isotropic_remeshing()` typedef unspecified_type FT; /// @} @@ -37,19 +37,18 @@ typedef unspecified_type FT; std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// called to check whether the halfedge with end vertices `va` and `vb` is longer -/// than the target edge size and as such should be split. If the halfedge is longer, -/// it returns the squared length of the edge. +/// a function controlling edge split and edge collapse, +/// returning the squared distance between the points of `va` and `vb` +/// if an edge between `va` and `vb` would be too long, and `std::nullopt` otherwise. std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const; -/// called to check whether the halfedge `h` should be collapsed in case it is -/// shorter than the target edge size. +/// a function controlling edge collapse by returning the squared length of `h` +/// if it is too short, and `std::nullopt` otherwise. std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// called to define the location of the halfedge `h` split in case `is_too_long()` -/// returns a value. +/// a function returning the location of the split point of the edge of `h`. Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const; /// @} diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index 74bfc24e231b..f5bb6f56231b 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -79,7 +79,7 @@ \cgalPkgPicture{hole_filling_ico.png} \cgalPkgSummaryBegin -\cgalPkgAuthors{David Coeurjolly, Jaques-Olivier Lachaud, Konstantinos Katriopla, Sébastien Loriot, Mael Rouxel-Labbé, Hossam Saeed, Jane Tournois, and Ilker %O. Yaz} +\cgalPkgAuthors{David Coeurjolly, Jaques-Olivier Lachaud, Konstantinos Katrioplas, Sébastien Loriot, Ivan Pađen, Mael Rouxel-Labbé, Hossam Saeed, Jane Tournois, and Ilker %O. Yaz} \cgalPkgDesc{This package provides a collection of methods and classes for polygon mesh processing, ranging from basic operations on simplices, to complex geometry processing algorithms such as Boolean operations, remeshing, repairing, collision and intersection detection, and more.} 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 0f89da2c013f..3b9fa8832637 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 @@ -4,7 +4,7 @@ namespace CGAL { \anchor Chapter_PolygonMeshProcessing \cgalAutoToc -\authors David Coeurjolly, Jaques-Olivier Lachaud, Konstantinos Katriopla, Sébastien Loriot, Mael Rouxel-Labbé, Hossam Saeed, Jane Tournois, and Ilker %O. Yaz +\authors David Coeurjolly, Jaques-Olivier Lachaud, Konstantinos Katrioplas, Sébastien Loriot, Ivan Pađen, Mael Rouxel-Labbé, Hossam Saeed, Jane Tournois, and Ilker %O. Yaz \image html neptun_head.jpg \image latex neptun_head.jpg @@ -120,10 +120,9 @@ by \cgalFigureRef{iso_remeshing}. The algorithm has two parameters: the sizing field object for the remeshed surface patch, and the number of iterations of the abovementioned sequence of operations. -The sizing field establishes the target edge length for the remeshed surface. The sizing field can be uniform or -adaptive. With the uniform sizing field, initiated by the `CGAL::Polygon_mesh_processing::Uniform_sizing_field()` constructor, -all triangle edges are targeted to have equal lengths. On the other hand, with the adaptive sizing field, initiated by -the `CGAL::Polygon_mesh_processing::Adaptive_sizing_field()`, triangle edge lengths depend on the local curvature -- +The sizing field establishes the local target edge length for the remeshed surface. Two sizing fields are +provided: a uniform and a curvature-adaptive sizing field. With `CGAL::Polygon_mesh_processing::Uniform_sizing_field`, +all triangle edges are targeted to have equal lengths. With `CGAL::Polygon_mesh_processing::Adaptive_sizing_field`, triangle edge lengths depend on the local curvature -- shorter edges appear in regions with a higher curvature and vice versa. The outline of the adaptive sizing field algorithm is available in \cgalCite{dunyach2013curvRemesh}. The distinction between uniform and adaptive sizing fields is depicted in figure \cgalFigureRef{uniform_and_adaptive}. @@ -1286,5 +1285,7 @@ supervision of David Coeurjolly, Jaques-Olivier Lachaud, and Sébastien Loriot. DGtal's implementation was also used as a reference during the project. +The curvature-based sizing field version of isotropic remeshing was added by Ivan Pađen during GSoC 2023, under the supervision of Sébastien Loriot and Jane Tournois. + */ } /* namespace CGAL */ diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 82a89b144dc5..3b9fd94f1895 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -35,7 +35,7 @@ namespace Polygon_mesh_processing * The local discrete curvatures are calculated using the * `CGAL::Polygon_mesh_processing::interpolated_corrected_principal_curvatures_and_directions()` function. * -* Edges longer than the local target edge length are split in half, while +* Edges longer than the local target edge length are split in two, while * edges shorter than the local target edge length are collapsed. * * \cgalModels{PMPSizingField} @@ -70,7 +70,7 @@ class Adaptive_sizing_field /// \name Creation /// @{ /*! - * returns an object to serve as criteria for adaptive curvature-based edge lengths. + * constructor * * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, * model of `Range`. Its iterator type is `ForwardIterator`. @@ -78,27 +78,19 @@ class Adaptive_sizing_field * * @param tol the error tolerance, used together with curvature to derive target edge length. * Lower tolerance values will result in shorter mesh edges. - * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed - * edge length. + * @param edge_len_min_max contains the bounds for minimum and maximum + * edge lengths * @param face_range the range of triangular faces defining one or several surface patches - * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. + * to be remeshed. It should be the same as the range of faces passed to `isotropic_remeshing()`. * \param vpmap is the input vertex point map that associates points to the vertices of - * an input mesh. + * the input mesh. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the - * same mesh as the one used in `isotropic_remeshing()`. + * same mesh as the one passed to `isotropic_remeshing()`. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin * \cgalParamNBegin{ball_radius} - * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. - * It can potentially smooth the curvature and consequently the sizing field in the - * case of noisy input.} - * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. - * It can effectively smooth the curvature field and consequently the sizing field.} - * \cgalParamType{`Base::FT`} - * \cgalParamDefault{`-1`} - * \cgalParamExtra{If this parameter is omitted (`-1`), the expansion will just by a weightless sum of - * measures on faces around the vertex.} + * \cgalParamDescription{`ball_radius` parameter passed to `interpolated_corrected_curvatures()`} * \cgalParamNEnd * \cgalNamedParamsEnd */ diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index e2e655aa1475..f6319f9fb1a6 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -27,8 +27,8 @@ namespace Polygon_mesh_processing * \ingroup PMP_meshing_grp * provides criteria for isotropic remeshing to achieve uniform mesh edge lengths. * -* Edges longer than the 4/3 of the target edge length will be split in half, while -* edges shorter than the 4/5 of the target edge length will be collapsed. +* Edges longer than 4/3 of the target edge length will be split in half, while +* edges shorter than 4/5 of the target edge length will be collapsed. * * \cgalModels{PMPSizingField} * @@ -38,7 +38,7 @@ namespace Polygon_mesh_processing * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. * @tparam VPMap a property map associating points to the vertices of `pmesh`. -* It is a a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* It is a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` * as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. */ template (const FT size, const VPMap& vpmap) : m_sq_short( CGAL::square(4./5. * size)) @@ -74,10 +74,9 @@ class Uniform_sizing_field {} /*! - * returns an object to serve as criterion for uniform edge lengths. It calls the first - * constructor using default values for the vertex point map of the input polygon mesh. + * Constructor using default values for the vertex point map of the input polygon mesh. * - * @param size is the target edge length for the isotropic remeshing. If set to 0, + * @param size the target edge length for isotropic remeshing. If set to 0, * the criterion for edge length is ignored and edges are neither split nor collapsed. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. The default * vertex point map of pmesh is used to construct the class. From 5c093c24914c453df6dc8fa02f37b192e12762ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 21:11:44 -0600 Subject: [PATCH 098/124] Fix doc issue --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 2 +- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index c9e66b1d2745..b27f3a536a9f 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -37,7 +37,7 @@ typedef unspecified_type FT; std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const; -/// a function controlling edge split and edge collapse, +/// a function controlling edge split and edge collapse, /// returning the squared distance between the points of `va` and `vb` /// if an edge between `va` and `vb` would be too long, and `std::nullopt` otherwise. std::optional is_too_long(const vertex_descriptor va, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 3b9fd94f1895..8f5e8079de6d 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -70,7 +70,7 @@ class Adaptive_sizing_field /// \name Creation /// @{ /*! - * constructor + * Constructor * * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, * model of `Range`. Its iterator type is `ForwardIterator`. @@ -78,7 +78,7 @@ class Adaptive_sizing_field * * @param tol the error tolerance, used together with curvature to derive target edge length. * Lower tolerance values will result in shorter mesh edges. - * @param edge_len_min_max contains the bounds for minimum and maximum + * @param edge_len_min_max contains the bounds for minimum and maximum * edge lengths * @param face_range the range of triangular faces defining one or several surface patches * to be remeshed. It should be the same as the range of faces passed to `isotropic_remeshing()`. From 98c64c351321c3bfcf5dc22b61c6d6a5e405b41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:04:13 -0600 Subject: [PATCH 099/124] Update is_too_long to work only with vertex descriptors --- .../Concepts/PMPSizingField.h | 6 ----- .../Adaptive_sizing_field.h | 13 ---------- .../Sizing_field_base.h | 2 -- .../Uniform_sizing_field.h | 9 ------- .../Isotropic_remeshing/remesh_impl.h | 26 +++++++++++-------- 5 files changed, 15 insertions(+), 41 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index b27f3a536a9f..6e5ca3f5c330 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -31,12 +31,6 @@ typedef unspecified_type FT; /// @name Functions /// @{ -/// called to check whether the halfedge `h` is longer than the target edge size -/// and as such should be split. If the halfedge is longer, it returns the squared -/// length of the edge. -std::optional is_too_long(const halfedge_descriptor h, - const PolygonMesh& pmesh) const; - /// a function controlling edge split and edge collapse, /// returning the squared distance between the points of `va` and `vb` /// if an edge between `va` and `vb` would be too long, and `std::nullopt` otherwise. diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 8f5e8079de6d..4fafa678caa9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -255,19 +255,6 @@ class Adaptive_sizing_field return get(m_vertex_sizing_map, v); } - std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const - { - const FT sqlen = sqlength(h, pmesh); - FT sqtarg_len = CGAL::square(4./3. * (CGAL::min)(get(m_vertex_sizing_map, source(h, pmesh)), - get(m_vertex_sizing_map, target(h, pmesh)))); - CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); - CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); - if(sqlen > sqtarg_len) - return sqlen; - else - return std::nullopt; - } - std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h index 8cb12191ad1a..c5a9d0b01bd1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h @@ -58,8 +58,6 @@ class Sizing_field_base typedef typename K::FT FT; public: - virtual std::optional is_too_long(const halfedge_descriptor h, - const PolygonMesh& pmesh) const = 0; virtual std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const = 0; virtual std::optional is_too_short(const halfedge_descriptor h, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index f6319f9fb1a6..99d98ea74596 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -100,15 +100,6 @@ class Uniform_sizing_field } public: - std::optional is_too_long(const halfedge_descriptor h, const PolygonMesh& pmesh) const - { - const FT sqlen = sqlength(h, pmesh); - if(sqlen > m_sq_long) - return sqlen; - else - return std::nullopt; - } - std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); 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 f7d93ab1e307..5a489c175050 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 @@ -399,9 +399,10 @@ namespace internal { ); for(edge_descriptor e : edge_range) { - std::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); + const halfedge_descriptor he = halfedge(e, mesh_); + std::optional sqlen = sizing.is_too_long(source(he, mesh_), target(he, mesh_)); if(sqlen != std::nullopt) - long_edges.emplace(halfedge(e, mesh_), sqlen.value()); + long_edges.emplace(he, sqlen.value()); } //split long edges @@ -437,13 +438,14 @@ namespace internal { //check sub-edges //if it was more than twice the "long" threshold, insert them - std::optional sqlen_new = sizing.is_too_long(hnew, mesh_); + std::optional sqlen_new = sizing.is_too_long(source(hnew, mesh_), target(hnew, mesh_)); if(sqlen_new != std::nullopt) long_edges.emplace(hnew, sqlen_new.value()); - sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); + const halfedge_descriptor hnext = next(hnew, mesh_); + sqlen_new = sizing.is_too_long(source(hnext, mesh_), target(hnext, mesh_)); if (sqlen_new != std::nullopt) - long_edges.emplace(next(hnew, mesh_), sqlen_new.value()); + long_edges.emplace(hnext, sqlen_new.value()); //insert new edges to keep triangular faces, and update long_edges if (!is_border(hnew, mesh_)) @@ -498,7 +500,8 @@ namespace internal { { if (!is_split_allowed(e)) continue; - std::optional sqlen = sizing.is_too_long(halfedge(e, mesh_), mesh_); + const halfedge_descriptor he = halfedge(e, mesh_); + std::optional sqlen = sizing.is_too_long(source(he, mesh_), target(he, mesh_)); if(sqlen != std::nullopt) long_edges.emplace(halfedge(e, mesh_), sqlen.value()); } @@ -553,13 +556,14 @@ namespace internal { //check sub-edges //if it was more than twice the "long" threshold, insert them - std::optional sqlen_new = sizing.is_too_long(hnew, mesh_); + std::optional sqlen_new = sizing.is_too_long(source(hnew, mesh_), target(hnew, mesh_)); if(sqlen_new != std::nullopt) long_edges.emplace(hnew, sqlen_new.value()); - sqlen_new = sizing.is_too_long(next(hnew, mesh_), mesh_); + const halfedge_descriptor hnext = next(hnew, mesh_); + sqlen_new = sizing.is_too_long(source(hnext, mesh_), target(hnext, mesh_)); if (sqlen_new != std::nullopt) - long_edges.emplace(next(hnew, mesh_), sqlen_new.value()); + long_edges.emplace(hnext, sqlen_new.value()); //insert new edges to keep triangular faces, and update long_edges if (!is_on_border(hnew)) @@ -578,7 +582,7 @@ namespace internal { if (snew == PATCH) { - std::optional sql = sizing.is_too_long(hnew2, mesh_); + std::optional sql = sizing.is_too_long(source(hnew2, mesh_), target(hnew2, mesh_)); if(sql != std::nullopt) long_edges.emplace(hnew2, sql.value()); } @@ -601,7 +605,7 @@ namespace internal { if (snew == PATCH) { - std::optional sql = sizing.is_too_long(hnew2, mesh_); + std::optional sql = sizing.is_too_long(source(hnew2, mesh_), target(hnew2, mesh_)); if (sql != std::nullopt) long_edges.emplace(hnew2, sql.value()); } From dfc2390d714cd35cc0b58f616c19565d48134839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 28 Sep 2023 22:10:11 -0600 Subject: [PATCH 100/124] Move Sizing_field_base to internal --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 6 +++--- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 6 +++--- .../{ => internal}/Sizing_field_base.h | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) rename Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/{ => internal}/Sizing_field_base.h (98%) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 4fafa678caa9..3083b8179e12 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include #include @@ -50,11 +50,11 @@ template ::const_type> class Adaptive_sizing_field #ifndef DOXYGEN_RUNNING - : public Sizing_field_base +: public internal::Sizing_field_base #endif { private: - typedef Sizing_field_base Base; + typedef internal::Sizing_field_base Base; typedef typename CGAL::dynamic_vertex_property_t Vertex_property_tag; typedef typename boost::property_map::type VertexSizingMap; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 99d98ea74596..5e3b22078966 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -15,7 +15,7 @@ #include -#include +#include #include @@ -45,11 +45,11 @@ template ::const_type> class Uniform_sizing_field #ifndef DOXYGEN_RUNNING - : public Sizing_field_base +: public internal::Sizing_field_base #endif { private: - typedef Sizing_field_base Base; + typedef internal::Sizing_field_base Base; public: typedef typename Base::FT FT; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h similarity index 98% rename from Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h rename to Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h index c5a9d0b01bd1..a7f19a26dfb8 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h @@ -24,6 +24,8 @@ namespace CGAL { namespace Polygon_mesh_processing { +namespace internal +{ /*! * \ingroup PMP_meshing_grp * pure virtual class serving as a base for sizing field classes utilized in isotropic @@ -66,6 +68,7 @@ class Sizing_field_base }; +}//end namespace internal }//end namespace Polygon_mesh_processing }//end namespace CGAL From 6b37280a0b32bc7f2388d43ff6e364bfcfe8d7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:01:10 -0600 Subject: [PATCH 101/124] Make is_too_short() and is_too_long() return edge-to-target ratio --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 8 ++++---- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 4 ++-- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index 6e5ca3f5c330..8cdfb541ed6e 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -32,13 +32,13 @@ typedef unspecified_type FT; /// @{ /// a function controlling edge split and edge collapse, -/// returning the squared distance between the points of `va` and `vb` -/// if an edge between `va` and `vb` would be too long, and `std::nullopt` otherwise. +/// returning the ratio of the current edge length and the local target edge length between +/// the points of `va` and `vb` in case the current edge is too long, and `std::nullopt` otherwise. std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const; -/// a function controlling edge collapse by returning the squared length of `h` -/// if it is too short, and `std::nullopt` otherwise. +/// a function controlling edge collapse by returning the ratio of the squared length of `h` and the +/// local target edge length if it is too short, and `std::nullopt` otherwise. std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 3083b8179e12..f90fa84a2bf3 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -263,7 +263,7 @@ class Adaptive_sizing_field CGAL_assertion(get(m_vertex_sizing_map, va)); CGAL_assertion(get(m_vertex_sizing_map, vb)); if (sqlen > sqtarg_len) - return sqlen; + return sqlen / sqtarg_len; else return std::nullopt; } @@ -276,7 +276,7 @@ class Adaptive_sizing_field CGAL_assertion(get(m_vertex_sizing_map, source(h, pmesh))); CGAL_assertion(get(m_vertex_sizing_map, target(h, pmesh))); if (sqlen < sqtarg_len) - return sqlen; + return sqlen / sqtarg_len; else return std::nullopt; } diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 5e3b22078966..c3084a34be67 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -104,6 +104,7 @@ class Uniform_sizing_field { const FT sqlen = sqlength(va, vb); if (sqlen > m_sq_long) + //no need to return the ratio for the uniform field return sqlen; else return std::nullopt; @@ -113,6 +114,7 @@ class Uniform_sizing_field { const FT sqlen = sqlength(h, pmesh); if (sqlen < m_sq_short) + //no need to return the ratio for the uniform field return sqlen; else return std::nullopt; From 3b4af4be313f7c8e84ec3f0e124ffdcb779c1e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:09:44 -0600 Subject: [PATCH 102/124] Update the sizing field updating function --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 8 ++++++-- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 3 +++ .../internal/Isotropic_remeshing/remesh_impl.h | 6 ++---- .../Polygon_mesh_processing/internal/Sizing_field_base.h | 1 + 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index 8cdfb541ed6e..5b90dddfbc92 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -35,16 +35,20 @@ typedef unspecified_type FT; /// returning the ratio of the current edge length and the local target edge length between /// the points of `va` and `vb` in case the current edge is too long, and `std::nullopt` otherwise. std::optional is_too_long(const vertex_descriptor va, - const vertex_descriptor vb) const; + const vertex_descriptor vb) const; /// a function controlling edge collapse by returning the ratio of the squared length of `h` and the /// local target edge length if it is too short, and `std::nullopt` otherwise. std::optional is_too_short(const halfedge_descriptor h, - const PolygonMesh& pmesh) const; + const PolygonMesh& pmesh) const; /// a function returning the location of the split point of the edge of `h`. Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const; + +/// a function that updates the sizing field value at the vertex `v`. +void update(const vertex_descriptor v, const PolygonMesh& pmesh); + /// @} }; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index f90fa84a2bf3..df9f21fec20a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -287,7 +287,7 @@ class Adaptive_sizing_field get(m_vpmap, source(h, pmesh))); } - void update_sizing_map(const vertex_descriptor v, const PolygonMesh& pmesh) + void update(const vertex_descriptor v, const PolygonMesh& pmesh) { // calculating it as the average of two vertices on other ends // of halfedges as updating is done during an edge split diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index c3084a34be67..b3a501486275 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -126,6 +126,9 @@ class Uniform_sizing_field get(m_vpmap, source(h, pmesh))); } + void update(const vertex_descriptor v, const PolygonMesh& pmesh) + {} + private: FT m_sq_short; FT m_sq_long; 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 5a489c175050..bb5ba5baeac8 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 @@ -433,8 +433,7 @@ namespace internal { std::cout << " refinement point : " << refinement_point << std::endl; #endif //update sizing field with the new point - if constexpr (!std::is_same_v>) - sizing.update_sizing_map(vnew, mesh_); + sizing.update(vnew, mesh_); //check sub-edges //if it was more than twice the "long" threshold, insert them @@ -551,8 +550,7 @@ namespace internal { halfedge_added(hnew_opp, status(opposite(he, mesh_))); //update sizing field with the new point - if constexpr (!std::is_same_v>) - sizing.update_sizing_map(vnew, mesh_); + sizing.update(vnew, mesh_); //check sub-edges //if it was more than twice the "long" threshold, insert them diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h index a7f19a26dfb8..3835f1803585 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h @@ -65,6 +65,7 @@ class Sizing_field_base virtual std::optional is_too_short(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; virtual Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const = 0; + virtual void update(const vertex_descriptor v, const PolygonMesh& pmesh) = 0; }; From e9aa5b9b3b84f24faace3b68b5979ff1baa8e53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:40:18 -0600 Subject: [PATCH 103/124] Replace vpmap constructor with np in Adaptive_sizing_field --- .../Adaptive_sizing_field.h | 61 ++++--------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index df9f21fec20a..61a9545d55e3 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -82,13 +82,21 @@ class Adaptive_sizing_field * edge lengths * @param face_range the range of triangular faces defining one or several surface patches * to be remeshed. It should be the same as the range of faces passed to `isotropic_remeshing()`. - * \param vpmap is the input vertex point map that associates points to the vertices of - * the input mesh. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the * same mesh as the one passed to `isotropic_remeshing()`. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin + * \cgalParamNBegin{vertex_point_map} + * \cgalParamDescription{a property map associating points to the vertices of `pmesh`} + * \cgalParamType{a class model of `ReadWritePropertyMap` with + * `boost::graph_traits::%vertex_descriptor` + * as key type and `%Point_3` as value type} + * \cgalParamDefault{`boost::get(CGAL::vertex_point, pmesh)`} + * \cgalParamExtra{If this parameter is omitted, an internal property map for `CGAL::vertex_point_t` + * must be available in `PolygonMesh`.} + * \cgalParamNEnd + * * \cgalParamNBegin{ball_radius} * \cgalParamDescription{`ball_radius` parameter passed to `interpolated_corrected_curvatures()`} * \cgalParamNEnd @@ -99,13 +107,14 @@ class Adaptive_sizing_field Adaptive_sizing_field(const FT tol , const std::pair& edge_len_min_max , const FaceRange& face_range - , const VPMap& vpmap , PolygonMesh& pmesh , const NamedParameters& np = parameters::default_values()) : tol(tol) , m_short(edge_len_min_max.first) , m_long(edge_len_min_max.second) - , m_vpmap(vpmap) + , m_vpmap(parameters::choose_parameter( + parameters::get_parameter(np, internal_np::vertex_point), + get_property_map(vertex_point, pmesh))) , m_vertex_sizing_map(get(Vertex_property_tag(), pmesh)) { if (face_range.size() == faces(pmesh).size()) @@ -128,50 +137,6 @@ class Adaptive_sizing_field } } - /*! - * returns an object to serve as criteria for adaptive curvature-based edge lengths. It calls - * the first constructor using default values for the vertex point map of the input polygon mesh. - * - * @tparam FaceRange range of `boost::graph_traits::%face_descriptor`, - * model of `Range`. Its iterator type is `ForwardIterator`. - * @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" - * - * @param tol the error tolerance, used together with curvature to derive target edge length. - * Lower tolerance values will result in shorter mesh edges. - * @param edge_len_min_max is the stopping criterion for minimum and maximum allowed - * edge length. - * @param face_range the range of triangular faces defining one or several surface patches - * to be remeshed. It should be the same as the range of faces used for `isotropic_remeshing()`. - * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. It should be the - * same mesh as the one used in `isotropic_remeshing()`. Additionally, the default vertex point - * map of pmesh is used to construct the class. - * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below - - * \cgalNamedParamsBegin - * \cgalParamNBegin{ball_radius} - * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. - * It can potentially smooth the curvature and consequently the sizing field in the - * case of noisy input.} - * \cgalParamDescription{a scalar value specifying the radius used for expanding curvature measures. - * It can effectively smooth the curvature field and consequently the sizing field.} - * \cgalParamType{`Base::FT`} - * \cgalParamDefault{`-1`} - * \cgalParamExtra{If this parameter is omitted (`-1`), the expansion will just by a weightless sum of - * measures on faces around the vertex.} - * \cgalParamNEnd - * \cgalNamedParamsEnd - */ - template - Adaptive_sizing_field(const FT tol - , const std::pair& edge_len_min_max - , const FaceRange& face_range - , PolygonMesh& pmesh - , const NamedParameters& np = parameters::default_values()) - : Adaptive_sizing_field(tol, edge_len_min_max, face_range, - get(CGAL::vertex_point, pmesh), pmesh, np) - {} - ///@} private: From faaeba8466f85c6a2c5796f6c370c7ce8e5bb335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 29 Sep 2023 22:21:41 -0600 Subject: [PATCH 104/124] Use interpolated_corrected_curvatures() with vertex property map --- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 61a9545d55e3..91e04160d113 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -33,7 +33,7 @@ namespace Polygon_mesh_processing * of local discrete curvatures. * * The local discrete curvatures are calculated using the -* `CGAL::Polygon_mesh_processing::interpolated_corrected_principal_curvatures_and_directions()` function. +* `CGAL::Polygon_mesh_processing::interpolated_corrected_curvatures()` function. * * Edges longer than the local target edge length are split in two, while * edges shorter than the local target edge length are collapsed. @@ -163,9 +163,9 @@ class Adaptive_sizing_field #endif Vertex_curvature_map vertex_curvature_map = get(Vertex_curvature_tag(), face_graph); - interpolated_corrected_principal_curvatures_and_directions(face_graph, - vertex_curvature_map, - parameters::ball_radius(radius)); + interpolated_corrected_curvatures(face_graph, + parameters::vertex_principal_curvatures_and_directions_map(vertex_curvature_map) + .ball_radius(radius)); // calculate vertex sizing field L(x_i) from the curvature field for(vertex_descriptor v : vertices(face_graph)) { From ace77795a32e4c4e7276b154e85d5fad0cb2fe4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:11:37 -0600 Subject: [PATCH 105/124] Apply suggestions from code review Co-authored-by: Jane Tournois --- .../internal/Isotropic_remeshing/remesh_impl.h | 4 ++-- .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) 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 bb5ba5baeac8..3e59249af72c 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 @@ -386,8 +386,8 @@ namespace internal { { #ifdef CGAL_PMP_REMESHING_VERBOSE -// std::cout << "Split long edges (" << high << ")..."; -// std::cout.flush(); + std::cout << "Split long edges..."; + std::cout.flush(); #endif //collect long edges 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 43bb5605a27b..799d15d03ffc 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -203,7 +203,7 @@ template::%edge_descriptor` * as key type and `bool` as value type. It must be default constructible.} * \cgalParamDefault{a default property map where no edges are constrained} From 740648622f68e5f9797353d55d50ba088ab4c0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:34:03 -0600 Subject: [PATCH 106/124] Attempt to combine tangential realxations into overload --- .../PackageDescription.txt | 1 - .../Isotropic_remeshing/remesh_impl.h | 2 +- .../tangential_relaxation.h | 49 ++++++++++--------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index f5bb6f56231b..f82c5af569e2 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -129,7 +129,6 @@ The page \ref bgl_namedparameters "Named Parameters" describes their usage. - `CGAL::Polygon_mesh_processing::smooth_mesh()` (deprecated) - `CGAL::Polygon_mesh_processing::angle_and_area_smoothing()` - `CGAL::Polygon_mesh_processing::tangential_relaxation()` -- `CGAL::Polygon_mesh_processing::tangential_relaxation_with_sizing()` - `CGAL::Polygon_mesh_processing::smooth_shape()` - `CGAL::Polygon_mesh_processing::random_perturbation()` 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 3e59249af72c..499bbd6c4e30 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 @@ -1071,7 +1071,7 @@ namespace internal { std::cout << " using tangential relaxation weighted with the sizing field"; std::cout << std::endl; #endif - tangential_relaxation_with_sizing( + tangential_relaxation( vertices(mesh_), mesh_, sizing, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index a77c5f1595b8..ae257f9eb57e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -119,10 +119,12 @@ struct Allow_all_moves{ * * \todo check if it should really be a triangle mesh or if a polygon mesh is fine */ -template +template void tangential_relaxation(const VertexRange& vertices, TriangleMesh& tm, - const NamedParameters& np = parameters::default_values()) + const CGAL_NP_CLASS& np = parameters::default_values()) { typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; @@ -131,17 +133,17 @@ void tangential_relaxation(const VertexRange& vertices, using parameters::get_parameter; using parameters::choose_parameter; - typedef typename GetGeomTraits::type GT; + typedef typename GetGeomTraits::type GT; GT gt = choose_parameter(get_parameter(np, internal_np::geom_traits), GT()); - typedef typename GetVertexPointMap::type VPMap; + typedef typename GetVertexPointMap::type VPMap; VPMap vpm = choose_parameter(get_parameter(np, internal_np::vertex_point), get_property_map(vertex_point, tm)); typedef Static_boolean_property_map Default_ECM; typedef typename internal_np::Lookup_named_param_def < internal_np::edge_is_constrained_t, - NamedParameters, + CGAL_NP_CLASS, Static_boolean_property_map // default (no constraint) > ::type ECM; ECM ecm = choose_parameter(get_parameter(np, internal_np::edge_is_constrained), @@ -149,7 +151,7 @@ void tangential_relaxation(const VertexRange& vertices, typedef typename internal_np::Lookup_named_param_def < internal_np::vertex_is_constrained_t, - NamedParameters, + CGAL_NP_CLASS, Static_boolean_property_map // default (no constraint) > ::type VCM; VCM vcm = choose_parameter(get_parameter(np, internal_np::vertex_is_constrained), @@ -201,7 +203,7 @@ void tangential_relaxation(const VertexRange& vertices, typedef typename internal_np::Lookup_named_param_def < internal_np::allow_move_functor_t, - NamedParameters, + CGAL_NP_CLASS, internal::Allow_all_moves// default > ::type Shall_move; Shall_move shall_move = choose_parameter(get_parameter(np, internal_np::allow_move_functor), @@ -281,8 +283,6 @@ void tangential_relaxation(const VertexRange& vertices, bary = squared_distance(p1, bary) -void tangential_relaxation_with_sizing(const VertexRange& vertices, - TriangleMesh& tm, - const SizingFunction& sizing, - const NamedParameters& np = parameters::default_values()) + typename CGAL_NP_TEMPLATE_PARAMETERS> +void tangential_relaxation(const VertexRange& vertices, + TriangleMesh& tm, + const SizingFunction& sizing, + const CGAL_NP_CLASS& np = parameters::default_values()) { typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; @@ -430,17 +430,17 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, using parameters::get_parameter; using parameters::choose_parameter; - typedef typename GetGeomTraits::type GT; + typedef typename GetGeomTraits::type GT; GT gt = choose_parameter(get_parameter(np, internal_np::geom_traits), GT()); - typedef typename GetVertexPointMap::type VPMap; + typedef typename GetVertexPointMap::type VPMap; VPMap vpm = choose_parameter(get_parameter(np, internal_np::vertex_point), get_property_map(vertex_point, tm)); typedef Static_boolean_property_map Default_ECM; typedef typename internal_np::Lookup_named_param_def < internal_np::edge_is_constrained_t, - NamedParameters, + CGAL_NP_CLASS, Static_boolean_property_map // default (no constraint) > ::type ECM; ECM ecm = choose_parameter(get_parameter(np, internal_np::edge_is_constrained), @@ -448,7 +448,7 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, typedef typename internal_np::Lookup_named_param_def < internal_np::vertex_is_constrained_t, - NamedParameters, + CGAL_NP_CLASS, Static_boolean_property_map // default (no constraint) > ::type VCM; VCM vcm = choose_parameter(get_parameter(np, internal_np::vertex_is_constrained), @@ -500,7 +500,7 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, typedef typename internal_np::Lookup_named_param_def < internal_np::allow_move_functor_t, - NamedParameters, + CGAL_NP_CLASS, internal::Allow_all_moves// default > ::type Shall_move; Shall_move shall_move = choose_parameter(get_parameter(np, internal_np::allow_move_functor), @@ -634,7 +634,8 @@ void tangential_relaxation_with_sizing(const VertexRange& vertices, */ template -void tangential_relaxation(TriangleMesh& tm, const CGAL_NP_CLASS& np = parameters::default_values()) +void tangential_relaxation(TriangleMesh& tm, + const CGAL_NP_CLASS& np = parameters::default_values()) { tangential_relaxation(vertices(tm), tm, np); } @@ -642,11 +643,11 @@ void tangential_relaxation(TriangleMesh& tm, const CGAL_NP_CLASS& np = parameter template -void tangential_relaxation_with_sizing(TriangleMesh& tm, - const SizingFunction& sizing, - const CGAL_NP_CLASS& np = parameters::default_values()) +void tangential_relaxation(TriangleMesh& tm, + const SizingFunction& sizing, + const CGAL_NP_CLASS& np = parameters::default_values()) { - tangential_relaxation_with_sizing(vertices(tm), tm, sizing, np); + tangential_relaxation(vertices(tm), tm, sizing, np); } } } // CGAL::Polygon_mesh_processing From e885155c229162ca645b6fd649ddd0e4d70031cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 3 Oct 2023 08:55:36 +0200 Subject: [PATCH 107/124] fix warnings --- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 2 +- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index b3a501486275..716729125f86 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -126,7 +126,7 @@ class Uniform_sizing_field get(m_vpmap, source(h, pmesh))); } - void update(const vertex_descriptor v, const PolygonMesh& pmesh) + void update(const vertex_descriptor /* v */, const PolygonMesh& /* pmesh */) {} private: diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 02c61ba52c2e..902fa9a0e7c9 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -1116,8 +1116,8 @@ public Q_SLOTS: const unsigned int nb_iter, const bool protect, const bool smooth_features) - : edges_only_(edges_only) - , edge_sizing_type_(edge_sizing_type) + : edge_sizing_type_(edge_sizing_type) + , edges_only_(edges_only) , target_length_(target_length) , error_tol_(error_tol) , min_length_(min_length) @@ -1128,8 +1128,8 @@ public Q_SLOTS: {} Remesh_polyhedron_item(const Remesh_polyhedron_item& remesh) - : edges_only_(remesh.edges_only_) - , edge_sizing_type_(remesh.edge_sizing_type_) + : edge_sizing_type_(remesh.edge_sizing_type_) + , edges_only_(remesh.edges_only_) , target_length_(remesh.target_length_) , error_tol_(remesh.error_tol_) , min_length_(remesh.min_length_) From 34c126839f410ec8cac8123866d0ac8a2d388d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:38:36 -0600 Subject: [PATCH 108/124] Address warnings --- .../Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp index 902fa9a0e7c9..c87fb567631d 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/PMP/Isotropic_remeshing_plugin.cpp @@ -396,7 +396,6 @@ public Q_SLOTS: unsigned int nb_smooth = ui.nbSmoothing_spinbox->value(); bool protect = ui.protect_checkbox->isChecked(); bool smooth_features = ui.smooth1D_checkbox->isChecked(); - bool curv_smooth = ui.curvSmooth_checkbox->isChecked(); double curv_ball_r = ui.curvSmoothBallR_edit->value(); // wait cursor @@ -871,8 +870,6 @@ public Q_SLOTS: unsigned int nb_iter = 1; bool protect = false; bool smooth_features = true; - bool curv_smooth = false; - double curv_ball_r = -1; std::vector selection; for(int index : scene->selectionIndices()) @@ -913,8 +910,6 @@ public Q_SLOTS: nb_iter = ui.nbIterations_spinbox->value(); protect = ui.protect_checkbox->isChecked(); smooth_features = ui.smooth1D_checkbox->isChecked(); - curv_smooth = ui.curvSmooth_checkbox->isChecked(); - curv_ball_r = ui.curvSmoothBallR_edit->value(); } } } @@ -1024,7 +1019,6 @@ public Q_SLOTS: unsigned int nb_iter_; bool protect_; bool smooth_features_; - bool curv_smooth_; double curv_ball_r_; protected: From 627a36fac7f546145e2c023601f007a0194fb614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:36:12 -0600 Subject: [PATCH 109/124] Combining tangential relaxations in one function WIP --- .../Isotropic_remeshing/remesh_impl.h | 4 +- .../tangential_relaxation.h | 58 ++++++++++++++++--- .../internal/parameters_interface.h | 1 + 3 files changed, 52 insertions(+), 11 deletions(-) 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 499bbd6c4e30..c1696428d756 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 @@ -1048,7 +1048,7 @@ namespace internal { auto constrained_vertices_pmap = boost::make_function_property_map(vertex_constraint); - if constexpr (std::is_same>::value) + if constexpr (std::is_same_v>) { #ifdef CGAL_PMP_REMESHING_VERBOSE std::cout << " using tangential relaxation with weights equal to 1"; @@ -1074,13 +1074,13 @@ namespace internal { tangential_relaxation( vertices(mesh_), mesh_, - sizing, CGAL::parameters::number_of_iterations(nb_iterations) .vertex_point_map(vpmap_) .geom_traits(gt_) .edge_is_constrained_map(constrained_edges_pmap) .vertex_is_constrained_map(constrained_vertices_pmap) .relax_constraints(relax_constraints) + .sizing_function(sizing) ); } diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index ae257f9eb57e..eb0f0881dab0 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -132,6 +133,7 @@ void tangential_relaxation(const VertexRange& vertices, using parameters::get_parameter; using parameters::choose_parameter; + using parameters::is_default_parameter; typedef typename GetGeomTraits::type GT; GT gt = choose_parameter(get_parameter(np, internal_np::geom_traits), GT()); @@ -157,6 +159,14 @@ void tangential_relaxation(const VertexRange& vertices, VCM vcm = choose_parameter(get_parameter(np, internal_np::vertex_is_constrained), Static_boolean_property_map()); + typedef typename internal_np::Lookup_named_param_def < + internal_np::vertex_is_constrained_t, + CGAL_NP_CLASS, + Uniform_sizing_field + > ::type SizingFunction; + SizingFunction sizing = choose_parameter(get_parameter(np, internal_np::sizing_function), + Uniform_sizing_field(-1, tm));//todo ip just a placeholder + const bool relax_constraints = choose_parameter(get_parameter(np, internal_np::relax_constraints), false); const unsigned int nb_iterations = choose_parameter(get_parameter(np, internal_np::number_of_iterations), 1); @@ -222,6 +232,12 @@ void tangential_relaxation(const VertexRange& vertices, auto gt_barycenter = gt.construct_barycenter_3_object(); auto gt_project = gt.construct_projected_point_3_object(); +// if constexpr (is_default_parameter::value) +// { +// auto gt_area = gt.compute_area_3_object(); +// auto gt_centroid = gt.construct_centroid_3_object(); +// } + // at each vertex, compute vertex normal std::unordered_map vnormals; compute_vertex_normals(tm, boost::make_assoc_property_map(vnormals), np); @@ -246,15 +262,39 @@ void tangential_relaxation(const VertexRange& vertices, { const Vector_3& vn = vnormals.at(v); Vector_3 move = CGAL::NULL_VECTOR; - unsigned int star_size = 0; - for(halfedge_descriptor h :interior_hedges) + if constexpr (std::is_same_v>) + { + unsigned int star_size = 0; + for(halfedge_descriptor h :interior_hedges) + { + move = move + Vector_3(get(vpm, v), get(vpm, source(h, tm))); + ++star_size; + } + CGAL_assertion(star_size > 0); //isolated vertices have already been discarded + move = (1. / static_cast(star_size)) * move; + } + else { - move = move + Vector_3(get(vpm, v), get(vpm, source(h, tm))); - ++star_size; + auto gt_centroid = gt.construct_centroid_3_object(); + auto gt_area = gt.compute_area_3_object(); + double weight = 0; + for(halfedge_descriptor h :interior_hedges) + { + // calculate weight + // need v, v1 and v2 + const vertex_descriptor v1 = target(next(h, tm), tm); + const vertex_descriptor v2 = source(h, tm); + + const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); + const double face_weight = tri_area + / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); + weight += face_weight; + + const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); + move = move + Vector_3(get(vpm, v), centroid) * face_weight; + } + move = move / weight; //todo ip: what if weight ends up being close to 0? } - CGAL_assertion(star_size > 0); //isolated vertices have already been discarded - move = (1. / static_cast(star_size)) * move; - barycenters.emplace_back(v, vn, get(vpm, v) + move); } else @@ -418,7 +458,7 @@ template -void tangential_relaxation(const VertexRange& vertices, +void tangential_relaxation_with_sizing(const VertexRange& vertices, TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) @@ -643,7 +683,7 @@ void tangential_relaxation(TriangleMesh& tm, template -void tangential_relaxation(TriangleMesh& tm, +void tangential_relaxation_with_sizing(TriangleMesh& tm, const SizingFunction& sizing, const CGAL_NP_CLASS& np = parameters::default_values()) { diff --git a/STL_Extension/include/CGAL/STL_Extension/internal/parameters_interface.h b/STL_Extension/include/CGAL/STL_Extension/internal/parameters_interface.h index cf196415595d..cb9c300ff6cb 100644 --- a/STL_Extension/include/CGAL/STL_Extension/internal/parameters_interface.h +++ b/STL_Extension/include/CGAL/STL_Extension/internal/parameters_interface.h @@ -161,6 +161,7 @@ CGAL_add_named_parameter(vertex_corner_map_t, vertex_corner_map, vertex_corner_m CGAL_add_named_parameter(patch_normal_map_t, patch_normal_map, patch_normal_map) CGAL_add_named_parameter(region_primitive_map_t, region_primitive_map, region_primitive_map) CGAL_add_named_parameter(postprocess_regions_t, postprocess_regions, postprocess_regions) +CGAL_add_named_parameter(sizing_function_t, sizing_function, sizing_function) // 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) From 6a4dbe5b59411c232259b2fb2b6933d2616fba9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 6 Oct 2023 08:59:29 +0200 Subject: [PATCH 110/124] fix copy/paste error --- .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index eb0f0881dab0..5c3d4fc8ad55 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -160,12 +160,12 @@ void tangential_relaxation(const VertexRange& vertices, Static_boolean_property_map()); typedef typename internal_np::Lookup_named_param_def < - internal_np::vertex_is_constrained_t, + internal_np::sizing_function_t, CGAL_NP_CLASS, Uniform_sizing_field > ::type SizingFunction; SizingFunction sizing = choose_parameter(get_parameter(np, internal_np::sizing_function), - Uniform_sizing_field(-1, tm));//todo ip just a placeholder + Uniform_sizing_field(-1, tm));//todo ip just a placeholder const bool relax_constraints = choose_parameter(get_parameter(np, internal_np::relax_constraints), false); const unsigned int nb_iterations = choose_parameter(get_parameter(np, internal_np::number_of_iterations), 1); From 618fb4b0271bda4fe06002f156f7145be3da91cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Fri, 6 Oct 2023 09:23:46 +0200 Subject: [PATCH 111/124] explicit template parameter to avoid ambiguity --- .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 5c3d4fc8ad55..e41adb9ca89a 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -165,7 +165,7 @@ void tangential_relaxation(const VertexRange& vertices, Uniform_sizing_field > ::type SizingFunction; SizingFunction sizing = choose_parameter(get_parameter(np, internal_np::sizing_function), - Uniform_sizing_field(-1, tm));//todo ip just a placeholder + Uniform_sizing_field(-1, vpm)); const bool relax_constraints = choose_parameter(get_parameter(np, internal_np::relax_constraints), false); const unsigned int nb_iterations = choose_parameter(get_parameter(np, internal_np::number_of_iterations), 1); From c41a0e38c2035a35cb911cb356b8e7e966bada39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:00:11 -0600 Subject: [PATCH 112/124] Merge two tangential relaxation functions into one --- .../tangential_relaxation.h | 326 +----------------- .../remeshing_quality_test.cpp | 2 +- 2 files changed, 11 insertions(+), 317 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index e41adb9ca89a..83fdd5ff37f2 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -116,6 +116,13 @@ struct Allow_all_moves{ * \cgalParamDefault{If not provided, all moves are allowed.} * \cgalParamNEnd * +* \cgalParamNBegin{sizing_function} +* \cgalParamDescription{An object containing sizing field for individual vertices. +* Used to derive smoothing weights.} +* \cgalParamType{A model of `PMPSizingField`.} +* \cgalParamDefault{If not provided, smoothing weights are the same for all vertices.} +* \cgalParamNEnd +* * \cgalNamedParamsEnd * * \todo check if it should really be a triangle mesh or if a polygon mesh is fine @@ -232,12 +239,6 @@ void tangential_relaxation(const VertexRange& vertices, auto gt_barycenter = gt.construct_barycenter_3_object(); auto gt_project = gt.construct_projected_point_3_object(); -// if constexpr (is_default_parameter::value) -// { -// auto gt_area = gt.compute_area_3_object(); -// auto gt_centroid = gt.construct_centroid_3_object(); -// } - // at each vertex, compute vertex normal std::unordered_map vnormals; compute_vertex_normals(tm, boost::make_assoc_property_map(vnormals), np); @@ -287,7 +288,9 @@ void tangential_relaxation(const VertexRange& vertices, const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); const double face_weight = tri_area - / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); + / (1. / 3. * (sizing.get_sizing(v) + + sizing.get_sizing(v1) + + sizing.get_sizing(v2))); weight += face_weight; const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); @@ -341,305 +344,6 @@ void tangential_relaxation(const VertexRange& vertices, new_locations.emplace_back(v, qv + (nv * Vector_3(qv, pv)) * nv); } - // perform moves - for(const VP_pair& vp : new_locations) - { - const Point_3 initial_pos = get(vpm, vp.first); // make a copy on purpose - const Vector_3 move(initial_pos, vp.second); - - put(vpm, vp.first, vp.second); - - //check that no inversion happened - double frac = 1.; - while (frac > 0.03 //5 attempts maximum - && ( !check_normals(vp.first) - || !shall_move(vp.first, initial_pos, get(vpm, vp.first)))) //if a face has been inverted - { - frac = 0.5 * frac; - put(vpm, vp.first, initial_pos + frac * move);//shorten the move by 2 - } - if (frac <= 0.02) - put(vpm, vp.first, initial_pos);//cancel move - } - }//end for loop (nit == nb_iterations) - -#ifdef CGAL_PMP_TANGENTIAL_RELAXATION_VERBOSE - std::cout << "\rTangential relaxation : " - << nb_iterations << " iterations done." << std::endl; -#endif -} - -/*! -* \ingroup PMP_meshing_grp -* applies an iterative area-based tangential smoothing to the given range of vertices based on the -* underlying sizing field. -* Each vertex `v` is relocated to its weighted centroid, where weights depend on the area of the -* adjacent triangle and its averaged vertex sizing values. -* The relocation vector is projected back to the tangent plane to the surface at `v`, iteratively. -* The connectivity remains unchanged. -* -* @tparam TriangleMesh model of `FaceGraph` and `VertexListGraph`. -* The descriptor types `boost::graph_traits::%face_descriptor` -* and `boost::graph_traits::%halfedge_descriptor` must be -* models of `Hashable`. -* @tparam VertexRange range of `boost::graph_traits::%vertex_descriptor`, -* model of `Range`. Its iterator type is `ForwardIterator`. -* @tparam SizingFunction model of `PMPSizingField` -* @tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" -* -* @param vertices the range of vertices which will be relocated by relaxation -* @param tm the triangle mesh to which `vertices` belong -* @param sizing a map containing sizing field for individual vertices. -* Used to derive smoothing weights. -* @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below -* -* \cgalNamedParamsBegin -* \cgalParamNBegin{vertex_point_map} -* \cgalParamDescription{a property map associating points to the vertices of `tm`} -* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` -* as key type and `%Point_3` as value type} -* \cgalParamDefault{`boost::get(CGAL::vertex_point, tm)`} -* \cgalParamExtra{If this parameter is omitted, an internal property map for `CGAL::vertex_point_t` -* must be available in `TriangleMesh`.} -* \cgalParamNEnd -* -* \cgalParamNBegin{geom_traits} -* \cgalParamDescription{an instance of a geometric traits class} -* \cgalParamType{a class model of `Kernel`} -* \cgalParamDefault{a \cgal Kernel deduced from the `Point_3` type, using `CGAL::Kernel_traits`} -* \cgalParamExtra{The geometric traits class must be compatible with the vertex `Point_3` type.} -* \cgalParamExtra{Exact constructions kernels are not supported by this function.} -* \cgalParamNEnd -* -* \cgalParamNBegin{number_of_iterations} -* \cgalParamDescription{the number of smoothing iterations} -* \cgalParamType{unsigned int} -* \cgalParamDefault{`1`} -* \cgalParamNEnd -* -* \cgalParamNBegin{edge_is_constrained_map} -* \cgalParamDescription{a property map containing the constrained-or-not status of each edge of `tm`. -* The endpoints of a constrained edge cannot be moved by relaxation, unless `relax_constraints` is `true`. -* The endpoints of a constrained polyline can never be moved by relaxation. -* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%edge_descriptor` -* as key type and `bool` as value type. It must be default constructible.} -* \cgalParamDefault{a default property map where no edges are constrained} -* \cgalParamExtra{Boundary edges are always considered as constrained edges.} -* \cgalParamNEnd -* -* \cgalParamNBegin{vertex_is_constrained_map} -* \cgalParamDescription{a property map containing the constrained-or-not status of each vertex of `tm`. -* A constrained vertex cannot be modified during relaxation.} -* \cgalParamType{a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` -* as key type and `bool` as value type. It must be default constructible.} -* \cgalParamDefault{a default property map where no vertices are constrained} -* \cgalParamNEnd -* -* \cgalParamNBegin{relax_constraints} -* \cgalParamDescription{If `true`, the end vertices of the edges set as constrained -* in `edge_is_constrained_map` and boundary edges move along the -* constrained polylines they belong to.} -* \cgalParamType{Boolean} -* \cgalParamDefault{`false`} -* \cgalParamNEnd -* -* \cgalParamNBegin{allow_move_functor} -* \cgalParamDescription{A function object used to determinate if a vertex move should be allowed or not} -* \cgalParamType{Unary functor that provides `bool operator()(vertex_descriptor v, Point_3 src, Point_3 tgt)` returning `true` -* if the vertex `v` can be moved from `src` to `tgt`; `Point_3` being the value type of the vertex point map } -* \cgalParamDefault{If not provided, all moves are allowed.} -* \cgalParamNEnd -* -* \cgalNamedParamsEnd -* -* \todo check if it should really be a triangle mesh or if a polygon mesh is fine -*/ -template -void tangential_relaxation_with_sizing(const VertexRange& vertices, - TriangleMesh& tm, - const SizingFunction& sizing, - const CGAL_NP_CLASS& np = parameters::default_values()) -{ - typedef typename boost::graph_traits::vertex_descriptor vertex_descriptor; - typedef typename boost::graph_traits::halfedge_descriptor halfedge_descriptor; - typedef typename boost::graph_traits::edge_descriptor edge_descriptor; - - using parameters::get_parameter; - using parameters::choose_parameter; - - typedef typename GetGeomTraits::type GT; - GT gt = choose_parameter(get_parameter(np, internal_np::geom_traits), GT()); - - typedef typename GetVertexPointMap::type VPMap; - VPMap vpm = choose_parameter(get_parameter(np, internal_np::vertex_point), - get_property_map(vertex_point, tm)); - - typedef Static_boolean_property_map Default_ECM; - typedef typename internal_np::Lookup_named_param_def < - internal_np::edge_is_constrained_t, - CGAL_NP_CLASS, - Static_boolean_property_map // default (no constraint) - > ::type ECM; - ECM ecm = choose_parameter(get_parameter(np, internal_np::edge_is_constrained), - Default_ECM()); - - typedef typename internal_np::Lookup_named_param_def < - internal_np::vertex_is_constrained_t, - CGAL_NP_CLASS, - Static_boolean_property_map // default (no constraint) - > ::type VCM; - VCM vcm = choose_parameter(get_parameter(np, internal_np::vertex_is_constrained), - Static_boolean_property_map()); - - const bool relax_constraints = choose_parameter(get_parameter(np, internal_np::relax_constraints), false); - const unsigned int nb_iterations = choose_parameter(get_parameter(np, internal_np::number_of_iterations), 1); - - typedef typename GT::Vector_3 Vector_3; - typedef typename GT::Point_3 Point_3; - - auto check_normals = [&](vertex_descriptor v) - { - bool first_run = true; - Vector_3 prev = NULL_VECTOR, first = NULL_VECTOR; - halfedge_descriptor first_h = boost::graph_traits::null_halfedge(); - for (halfedge_descriptor hd : CGAL::halfedges_around_target(v, tm)) - { - if (is_border(hd, tm)) continue; - - Vector_3 n = compute_face_normal(face(hd, tm), tm, np); - if (n == CGAL::NULL_VECTOR) //for degenerate faces - continue; - - if (first_run) - { - first_run = false; - first = n; - first_h = hd; - } - else - { - if (!get(ecm, edge(hd, tm))) - if (to_double(n * prev) <= 0) - return false; - } - prev = n; - } - - if (first_run) - return true; //vertex incident only to degenerate faces - - if (!get(ecm, edge(first_h, tm))) - if (to_double(first * prev) <= 0) - return false; - - return true; - }; - - typedef typename internal_np::Lookup_named_param_def < - internal_np::allow_move_functor_t, - CGAL_NP_CLASS, - internal::Allow_all_moves// default - > ::type Shall_move; - Shall_move shall_move = choose_parameter(get_parameter(np, internal_np::allow_move_functor), - internal::Allow_all_moves()); - - for (unsigned int nit = 0; nit < nb_iterations; ++nit) - { -#ifdef CGAL_PMP_TANGENTIAL_RELAXATION_VERBOSE - std::cout << "\r\t(Tangential relaxation iteration " << (nit + 1) << " / "; - std::cout << nb_iterations << ") "; - std::cout.flush(); -#endif - - typedef std::tuple VNP; - std::vector< VNP > barycenters; - auto gt_barycenter = gt.construct_barycenter_3_object(); - auto gt_centroid = gt.construct_centroid_3_object(); - auto gt_area = gt.compute_area_3_object(); - - // at each vertex, compute vertex normal - std::unordered_map vnormals; - compute_vertex_normals(tm, boost::make_assoc_property_map(vnormals), np); - - // at each vertex, compute centroids of neighbouring faces weighted by - // area and sizing field - for(vertex_descriptor v : vertices) - { - if (get(vcm, v) || CGAL::internal::is_isolated(v, tm)) - continue; - - // collect hedges to detect if we have to handle boundary cases - std::vector interior_hedges, border_halfedges; - for(halfedge_descriptor h : halfedges_around_target(v, tm)) - { - if (is_border_edge(h, tm) || get(ecm, edge(h, tm))) - border_halfedges.push_back(h); - else - interior_hedges.push_back(h); - } - - if (border_halfedges.empty()) - { - const Vector_3& vn = vnormals.at(v); - Vector_3 move = CGAL::NULL_VECTOR; - double weight = 0; - for(halfedge_descriptor h :interior_hedges) - { - // calculate weight - // need v, v1 and v2 - const vertex_descriptor v1 = target(next(h, tm), tm); - const vertex_descriptor v2 = source(h, tm); - - const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); - const double face_weight = tri_area - / (1. / 3. * (sizing.get_sizing(v) + sizing.get_sizing(v1) + sizing.get_sizing(v2))); - weight += face_weight; - - const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); - move = move + Vector_3(get(vpm, v), centroid) * face_weight; - } - move = move / weight; //todo ip: what if weight ends up being close to 0? - - barycenters.emplace_back(v, vn, get(vpm, v) + move); - } - else - { - if (!relax_constraints) continue; - Vector_3 vn(NULL_VECTOR); - - if (border_halfedges.size() == 2)// corners are constrained - { - vertex_descriptor ph0 = source(border_halfedges[0], tm); - vertex_descriptor ph1 = source(border_halfedges[1], tm); - double dot = to_double(Vector_3(get(vpm, v), get(vpm, ph0)) - * Vector_3(get(vpm, v), get(vpm, ph1))); - // \todo shouldn't it be an input parameter? - //check squared cosine is < 0.25 (~120 degrees) - if (0.25 < dot*dot / ( squared_distance(get(vpm,ph0), get(vpm, v)) * - squared_distance(get(vpm,ph1), get(vpm, v))) ) - barycenters.emplace_back(v, vn, - gt_barycenter(get(vpm, ph0), 0.25, get(vpm, ph1), 0.25, get(vpm, v), 0.5)); - } - } - } - - // compute moves - typedef std::pair VP_pair; - std::vector< std::pair > new_locations; - new_locations.reserve(barycenters.size()); - for(const VNP& vnp : barycenters) - { - vertex_descriptor v = std::get<0>(vnp); - const Point_3& pv = get(vpm, v); - const Vector_3& nv = std::get<1>(vnp); - const Point_3& qv = std::get<2>(vnp); //barycenter at v - - new_locations.emplace_back(v, qv + (nv * Vector_3(qv, pv)) * nv); - } - // perform moves for(const VP_pair& vp : new_locations) { @@ -680,16 +384,6 @@ void tangential_relaxation(TriangleMesh& tm, tangential_relaxation(vertices(tm), tm, np); } -template -void tangential_relaxation_with_sizing(TriangleMesh& tm, - const SizingFunction& sizing, - const CGAL_NP_CLASS& np = parameters::default_values()) -{ - tangential_relaxation(vertices(tm), tm, sizing, np); -} - } } // CGAL::Polygon_mesh_processing #endif //CGAL_POLYGON_MESH_PROCESSING_TANGENTIAL_RELAXATION_H diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index a6216663596f..93b3c4cad05f 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -107,4 +107,4 @@ int main(int argc, char* argv[]) << " deg" << std::endl; return 0; -} \ No newline at end of file +} From dc36eb88a7be606b71128234c73d8901f8c3b7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:12:43 -0600 Subject: [PATCH 113/124] Apply suggestions from code review Co-authored-by: Jane Tournois --- .../doc/Polygon_mesh_processing/PackageDescription.txt | 4 ++-- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 8 ++++---- .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt index f82c5af569e2..b9e2ecaffc16 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/PackageDescription.txt @@ -133,8 +133,8 @@ The page \ref bgl_namedparameters "Named Parameters" describes their usage. - `CGAL::Polygon_mesh_processing::random_perturbation()` \cgalCRPSection{Sizing Fields} -- `CGAL::Polygon_mesh_processing::Uniform_sizing_field()` -- `CGAL::Polygon_mesh_processing::Adaptive_sizing_field()` +- `CGAL::Polygon_mesh_processing::Uniform_sizing_field` +- `CGAL::Polygon_mesh_processing::Adaptive_sizing_field` \cgalCRPSection{Orientation Functions} - `CGAL::Polygon_mesh_processing::orient_polygon_soup()` diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 716729125f86..d73b318f8c19 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -74,12 +74,12 @@ class Uniform_sizing_field {} /*! - * Constructor using default values for the vertex point map of the input polygon mesh. + * Constructor using internal vertex point map of the input polygon mesh. * * @param size the target edge length for isotropic remeshing. If set to 0, * the criterion for edge length is ignored and edges are neither split nor collapsed. * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. The default - * vertex point map of pmesh is used to construct the class. + * vertex point map of `pmesh` is used to construct the class. */ Uniform_sizing_field(const FT size, const PolygonMesh& pmesh) : Uniform_sizing_field(size, get(CGAL::vertex_point, pmesh)) @@ -130,8 +130,8 @@ class Uniform_sizing_field {} private: - FT m_sq_short; - FT m_sq_long; + const FT m_sq_short; + const FT m_sq_long; const VPMap m_vpmap; }; 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 799d15d03ffc..b9960450f293 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/remesh.h @@ -382,7 +382,7 @@ void isotropic_remeshing(const FaceRange& faces * @param pmesh a polygon mesh * @param edges the range of edges to be split if they are longer than given threshold * @param sizing the sizing function that is used to split edges from 'edges' list. If a number convertible to -* a double is passed, it will use a `Uniform_sizing_field()` with the number as a target edge length. +* a `double` is passed, it will use a `Uniform_sizing_field()` with the number as target edge length. * @param np an optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * \cgalNamedParamsBegin From 904c10016adcd306a4cbcb9ccecd3090758b47ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Tue, 10 Oct 2023 19:34:46 -0600 Subject: [PATCH 114/124] Add sizing.at function to the PMPSizingField and other sizing classes --- .../Polygon_mesh_processing/Concepts/PMPSizingField.h | 6 +++++- .../CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 2 +- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 9 ++++++++- .../Polygon_mesh_processing/internal/Sizing_field_base.h | 1 + .../CGAL/Polygon_mesh_processing/tangential_relaxation.h | 6 +++--- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index 5b90dddfbc92..d672f19bb063 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -31,6 +31,9 @@ typedef unspecified_type FT; /// @name Functions /// @{ +/// a function that returns the sizing value at `v`. +FT at(const vertex_descriptor v) const; + /// a function controlling edge split and edge collapse, /// returning the ratio of the current edge length and the local target edge length between /// the points of `va` and `vb` in case the current edge is too long, and `std::nullopt` otherwise. @@ -47,7 +50,8 @@ Point_3 split_placement(const halfedge_descriptor h, const PolygonMesh& pmesh) const; /// a function that updates the sizing field value at the vertex `v`. -void update(const vertex_descriptor v, const PolygonMesh& pmesh); +void update(const vertex_descriptor v, + const PolygonMesh& pmesh); /// @} }; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index 91e04160d113..cf46db6d9197 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -214,7 +214,7 @@ class Adaptive_sizing_field } public: - FT get_sizing(const vertex_descriptor v) const + FT at(const vertex_descriptor v) const { CGAL_assertion(get(m_vertex_sizing_map, v)); return get(m_vertex_sizing_map, v); diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index d73b318f8c19..00ea4a39dcb1 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -68,7 +68,8 @@ class Uniform_sizing_field * the input mesh. */ Uniform_sizing_field(const FT size, const VPMap& vpmap) - : m_sq_short( CGAL::square(4./5. * size)) + : m_size(size) + , m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) , m_vpmap(vpmap) {} @@ -100,6 +101,11 @@ class Uniform_sizing_field } public: + FT at(const vertex_descriptor /* v */) const + { + return m_size; + } + std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const { const FT sqlen = sqlength(va, vb); @@ -130,6 +136,7 @@ class Uniform_sizing_field {} private: + const FT m_size; const FT m_sq_short; const FT m_sq_long; const VPMap m_vpmap; diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h index 3835f1803585..1f70c251aca7 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h @@ -60,6 +60,7 @@ class Sizing_field_base typedef typename K::FT FT; public: + virtual FT at(const vertex_descriptor v) const; virtual std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const = 0; virtual std::optional is_too_short(const halfedge_descriptor h, diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h index 83fdd5ff37f2..6693524324a9 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/tangential_relaxation.h @@ -288,9 +288,9 @@ void tangential_relaxation(const VertexRange& vertices, const double tri_area = gt_area(get(vpm, v), get(vpm, v1), get(vpm, v2)); const double face_weight = tri_area - / (1. / 3. * (sizing.get_sizing(v) - + sizing.get_sizing(v1) - + sizing.get_sizing(v2))); + / (1. / 3. * (sizing.at(v) + + sizing.at(v1) + + sizing.at(v2))); weight += face_weight; const Point_3 centroid = gt_centroid(get(vpm, v), get(vpm, v1), get(vpm, v2)); From 1dbd8c281d8f325c9a0d47f972db12c0bb0a413b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Wed, 11 Oct 2023 07:21:12 -0700 Subject: [PATCH 115/124] make it pure virtual --- .../CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h index 1f70c251aca7..e62e86c9a83e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h @@ -60,7 +60,7 @@ class Sizing_field_base typedef typename K::FT FT; public: - virtual FT at(const vertex_descriptor v) const; + virtual FT at(const vertex_descriptor v) const = 0; virtual std::optional is_too_long(const vertex_descriptor va, const vertex_descriptor vb) const = 0; virtual std::optional is_too_short(const halfedge_descriptor h, From 4b9bd778ef99cff69c97154102a050cdc33d9844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Pa=C4=91en?= <49401914+ipadjen@users.noreply.github.com> Date: Fri, 20 Oct 2023 23:51:40 -0600 Subject: [PATCH 116/124] Apply suggestions from code review Co-authored-by: Jane Tournois --- .../Polygon_mesh_processing/Adaptive_sizing_field.h | 12 ++++++------ .../Polygon_mesh_processing/Uniform_sizing_field.h | 9 +++++---- .../internal/Sizing_field_base.h | 6 +++--- .../include/CGAL/Polygon_mesh_processing/remesh.h | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index cf46db6d9197..a98b22de4edc 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -29,14 +29,14 @@ namespace Polygon_mesh_processing { /*! * \ingroup PMP_meshing_grp -* provides criteria for isotropic remeshing to achieve variable mesh edge lengths as a function -* of local discrete curvatures. -* -* The local discrete curvatures are calculated using the +* a sizing field describing variable target mesh edge lengths for +* `CGAL::Polygon_mesh_processing::isotropic_remeshing()`. +* This adaptive sizing field is a function of local discrete curvatures, +* computed using the * `CGAL::Polygon_mesh_processing::interpolated_corrected_curvatures()` function. * -* Edges longer than the local target edge length are split in two, while -* edges shorter than the local target edge length are collapsed. +* Edges too long with respect to the local target edge length are split in two, while +* edges that are too short are collapsed. * * \cgalModels{PMPSizingField} * diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 00ea4a39dcb1..037a2a161877 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -25,7 +25,8 @@ namespace Polygon_mesh_processing { /*! * \ingroup PMP_meshing_grp -* provides criteria for isotropic remeshing to achieve uniform mesh edge lengths. +* a sizing field describing a uniform target edge length for +* `CGAL::Polygon_mesh_processing::isotropic_remeshing()`. * * Edges longer than 4/3 of the target edge length will be split in half, while * edges shorter than 4/5 of the target edge length will be collapsed. @@ -37,9 +38,9 @@ namespace Polygon_mesh_processing * * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. -* @tparam VPMap a property map associating points to the vertices of `pmesh`. -* It is a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` -* as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. +* @tparam VPMap property map associating points to the vertices of `pmesh`, +* model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. */ template ::const_type> diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h index e62e86c9a83e..f13135cd873e 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/internal/Sizing_field_base.h @@ -28,7 +28,7 @@ namespace internal { /*! * \ingroup PMP_meshing_grp -* pure virtual class serving as a base for sizing field classes utilized in isotropic +* pure virtual class serving as a base for sizing field classes used in isotropic * remeshing. * * \cgalModels{PMPSizingField} @@ -39,8 +39,8 @@ namespace internal * * @tparam PolygonMesh model of `MutableFaceGraph` that * has an internal property map for `CGAL::vertex_point_t`. -* @tparam VPMap a property map associating points to the vertices of `pmesh`. -* It is a a class model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` +* @tparam VPMap property map associating points to the vertices of `pmesh`, +* model of `ReadWritePropertyMap` with `boost::graph_traits::%vertex_descriptor` * as key type and `%Point_3` as value type. Default is `boost::get(CGAL::vertex_point, pmesh)`. */ template Date: Mon, 23 Oct 2023 17:46:38 +0200 Subject: [PATCH 117/124] fix compilation errors --- .../CGAL/Polygon_mesh_processing/Uniform_sizing_field.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h index 037a2a161877..a4144ceee650 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Uniform_sizing_field.h @@ -68,7 +68,7 @@ class Uniform_sizing_field * \param vpmap is the input vertex point map that associates points to the vertices of * the input mesh. */ - Uniform_sizing_field(const FT size, const VPMap& vpmap) + Uniform_sizing_field(const FT size, const VPMap& vpmap) : m_size(size) , m_sq_short( CGAL::square(4./5. * size)) , m_sq_long( CGAL::square(4./3. * size)) @@ -83,7 +83,7 @@ class Uniform_sizing_field * @param pmesh a polygon mesh with triangulated surface patches to be remeshed. The default * vertex point map of `pmesh` is used to construct the class. */ - Uniform_sizing_field(const FT size, const PolygonMesh& pmesh) + Uniform_sizing_field(const FT size, const PolygonMesh& pmesh) : Uniform_sizing_field(size, get(CGAL::vertex_point, pmesh)) {} From 29d948a788e85ef0a828c174d0517b1346bf9879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Mon, 23 Oct 2023 18:03:31 +0200 Subject: [PATCH 118/124] handle new letter --- Documentation/doc/scripts/generate_how_to_cite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/doc/scripts/generate_how_to_cite.py b/Documentation/doc/scripts/generate_how_to_cite.py index 71e1c6258c90..c2a422bdda28 100644 --- a/Documentation/doc/scripts/generate_how_to_cite.py +++ b/Documentation/doc/scripts/generate_how_to_cite.py @@ -157,7 +157,7 @@ def protect_upper_case(title): return title.replace("dD","{dD}").replace("2D","{2D}").replace("3D","{3D}").replace("CGAL","{CGAL}").replace("Qt","{Qt}").replace("Boost","{Boost}") def protect_accentuated_letters(authors): - res=authors.replace("é",r"{\'e}").replace("è",r"{\`e}").replace("É",r"{\'E}").replace("ä",r"{\"a}").replace("ö",r"{\"o}").replace("ñ",r"{\~n}").replace("ã",r"{\~a}").replace("ë",r"{\"e}").replace("ı",r"{\i}").replace("Ş",r"{\c{S}}").replace("ş",r"{\c{s}}").replace("%","") + res=authors.replace("é",r"{\'e}").replace("è",r"{\`e}").replace("É",r"{\'E}").replace("ä",r"{\"a}").replace("ö",r"{\"o}").replace("ñ",r"{\~n}").replace("ã",r"{\~a}").replace("ë",r"{\"e}").replace("ı",r"{\i}").replace("Ş",r"{\c{S}}").replace("ş",r"{\c{s}}").replace("%","").replace("đ",r"{\-d}") try: res.encode('ascii') except UnicodeEncodeError: From e4f9a57914f42349c87740914f845388806b95f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Tue, 24 Oct 2023 09:03:48 +0200 Subject: [PATCH 119/124] do not use deprecated API --- .../isotropic_remeshing_with_sizing_example.cpp | 4 ++-- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 1fc3c4960d38..6587f1e514bc 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -33,8 +33,8 @@ int main(int argc, char* argv[]) faces(mesh), sizing_field, mesh, - PMP::parameters::number_of_iterations(nb_iter) - .number_of_relaxation_steps(3) + PMP::number_of_iterations(nb_iter) + .number_of_relaxation_steps(3) ); CGAL::IO::write_polygon_mesh("out.off", mesh, CGAL::parameters::stream_precision(17)); diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index 93b3c4cad05f..eb60d2111cf1 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -36,7 +36,7 @@ int main(int argc, char* argv[]) faces(mesh), sizing_field, mesh, - PMP::parameters::number_of_iterations(nb_iter).number_of_relaxation_steps(3) + PMP::number_of_iterations(nb_iter).number_of_relaxation_steps(3) ); /* From 1e138a011bafb4208a3d2dbf2217204e2f68eaba Mon Sep 17 00:00:00 2001 From: Jane Tournois Date: Wed, 25 Oct 2023 17:21:18 +0200 Subject: [PATCH 120/124] fix compilation --- .../isotropic_remeshing_with_sizing_example.cpp | 4 ++-- .../test/Polygon_mesh_processing/remeshing_quality_test.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp index 6587f1e514bc..b4e2475f39d2 100644 --- a/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp +++ b/Polygon_mesh_processing/examples/Polygon_mesh_processing/isotropic_remeshing_with_sizing_example.cpp @@ -33,8 +33,8 @@ int main(int argc, char* argv[]) faces(mesh), sizing_field, mesh, - PMP::number_of_iterations(nb_iter) - .number_of_relaxation_steps(3) + CGAL::parameters::number_of_iterations(nb_iter) + .number_of_relaxation_steps(3) ); CGAL::IO::write_polygon_mesh("out.off", mesh, CGAL::parameters::stream_precision(17)); diff --git a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp index eb60d2111cf1..7ab20cda4246 100644 --- a/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp +++ b/Polygon_mesh_processing/test/Polygon_mesh_processing/remeshing_quality_test.cpp @@ -36,7 +36,7 @@ int main(int argc, char* argv[]) faces(mesh), sizing_field, mesh, - PMP::number_of_iterations(nb_iter).number_of_relaxation_steps(3) + CGAL::parameters::number_of_iterations(nb_iter).number_of_relaxation_steps(3) ); /* From 4c7bc4cbae8d94bac0175017c606a0b5233cde2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 16 Nov 2023 09:43:35 +0100 Subject: [PATCH 121/124] fix after rebase --- .../doc/Polygon_mesh_processing/Concepts/PMPSizingField.h | 7 +++++++ .../doc/Polygon_mesh_processing/Doxyfile.in | 5 ++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h index d672f19bb063..8641c30c6caf 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Concepts/PMPSizingField.h @@ -4,6 +4,13 @@ /// The concept `PMPSizingField` defines the requirements for the sizing field /// used in `CGAL::Polygon_mesh_processing::isotropic_remeshing()` to define /// the target length for every individual edge during the remeshing process. +/// +/// \cgalHasModelsBegin +/// \cgalHasModels{CGAL::Polygon_mesh_processing::Uniform_sizing_field} +/// \cgalHasModels{CGAL::Polygon_mesh_processing::Adaptive_sizing_field} +/// \cgalHasModelsEnd +/// + class PMPSizingField{ public: diff --git a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Doxyfile.in b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Doxyfile.in index c27c024648fe..797c895fa7e9 100644 --- a/Polygon_mesh_processing/doc/Polygon_mesh_processing/Doxyfile.in +++ b/Polygon_mesh_processing/doc/Polygon_mesh_processing/Doxyfile.in @@ -23,8 +23,7 @@ EXCLUDE_SYMBOLS += experimental HTML_EXTRA_FILES = ${CGAL_PACKAGE_DOC_DIR}/fig/selfintersections.jpg \ ${CGAL_PACKAGE_DOC_DIR}/fig/mesh_smoothing.png \ ${CGAL_PACKAGE_DOC_DIR}/fig/shape_smoothing.png \ - ${CGAL_PACKAGE_DOC_DIR}/fig/icc_diff_radius.png \ ${CGAL_PACKAGE_DOC_DIR}/fig/decimate_cheese.png \ ${CGAL_PACKAGE_DOC_DIR}/fig/decimate_colors.png \ - ${CGAL_PACKAGE_DOC_DIR}/fig/decimate_rg_joint.png - + ${CGAL_PACKAGE_DOC_DIR}/fig/decimate_rg_joint.png \ + ${CGAL_PACKAGE_DOC_DIR}/fig/shape_smoothing.png From 7328ed7fffb0a11a5ec188f73922424531cbedaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 16 Nov 2023 11:59:43 +0100 Subject: [PATCH 122/124] remove useless (and potentially dangerous) default --- .../interpolated_corrected_curvatures.h | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/interpolated_corrected_curvatures.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/interpolated_corrected_curvatures.h index 25de9ec9387f..a8d45d8b867f 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/interpolated_corrected_curvatures.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/interpolated_corrected_curvatures.h @@ -617,12 +617,11 @@ void set_value(const T&, internal_np::Param_not_found) // computes selected curvatures for one specific vertex template - void interpolated_corrected_curvatures_one_vertex( - const PolygonMesh& pmesh, - const typename boost::graph_traits::vertex_descriptor v, - const NamedParameters& np = parameters::default_values() - ) + typename NamedParameters> +void interpolated_corrected_curvatures_one_vertex( + const PolygonMesh& pmesh, + const typename boost::graph_traits::vertex_descriptor v, + const NamedParameters& np) { typedef typename GetGeomTraits::type GT; typedef typename GetVertexPointMap::const_type Vertex_position_map; @@ -716,7 +715,7 @@ template +template class Interpolated_corrected_curvatures_computer { typedef typename GetGeomTraits::type GT; @@ -780,7 +779,6 @@ class Interpolated_corrected_curvatures_computer mu1_map = get(CGAL::dynamic_face_property_t(), pmesh); mu2_map = get(CGAL::dynamic_face_property_t(), pmesh); muXY_map = get(CGAL::dynamic_face_property_t>(), pmesh); - } void set_named_params(const NamedParameters& np) @@ -824,10 +822,8 @@ class Interpolated_corrected_curvatures_computer public: - Interpolated_corrected_curvatures_computer(const PolygonMesh& pmesh, - const NamedParameters& np = parameters::default_values() - ) : - pmesh(pmesh) + Interpolated_corrected_curvatures_computer(const PolygonMesh& pmesh,const NamedParameters& np) + : pmesh(pmesh) { set_named_params(np); From 94d123491381d196fb2adc506858feff232cdc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loriot?= Date: Thu, 16 Nov 2023 12:00:35 +0100 Subject: [PATCH 123/124] restore Mael's version --- .../Plugins/Display/Display_property.ui | 290 ++++++++++-------- .../Display/Display_property_plugin.cpp | 192 +++++++++--- 2 files changed, 316 insertions(+), 166 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui index 656ec4827c55..0a824308300d 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property.ui @@ -3,7 +3,7 @@ DisplayPropertyWidget - false + true @@ -39,9 +39,18 @@ + + true + Property + + false + + + false + 6 @@ -82,13 +91,114 @@ + + + + true + + + 0 + + + 100 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksAbove + + + + + + + true + + + Expanding Radius: 0 + + + + + + + + + + false + + + Extreme Values + + + + + + Min Value + + + + + + + Max Value + + + + + + + 2.00 + + + true + + + + + + + Zoom to max value + + + + + + + Zoom to min value + + + + + + + 0.00 + + + true + + + true + + + - - - + + + Qt::Vertical @@ -100,7 +210,33 @@ - + + + + true + + + + + 0 + 0 + 236 + 335 + + + + + + + RAMP DISPLAYING + + + + + + + + Color Visualization @@ -118,10 +254,17 @@ 0 - - + + - Random colors + + + + + + + + Max color @@ -135,20 +278,6 @@ - - - - First color - - - - - - - Max color - - - @@ -159,32 +288,42 @@ - + - - + + - + Min color - - + + - Min color + First color + + + + + + + Random colors - - + + + + + Qt::Vertical @@ -196,97 +335,8 @@ - - - - true - - - - - 0 - 0 - 236 - 397 - - - - - - - RAMP DISPLAYING - - - - - - - - - - - false - - - Extreme Values - - - - - - Min Value - - - - - - - Max Value - - - - - - - 2.00 - - - true - - - - - - - Zoom to max value - - - - - - - Zoom to min value - - - - - - - 0.00 - - - true - - - true - - - - - - diff --git a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp index 687cb6a92dfd..fb06449e972d 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Display/Display_property_plugin.cpp @@ -28,11 +28,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -47,9 +47,9 @@ #define ARBITRARY_DBL_MIN 1.0E-17 #define ARBITRARY_DBL_MAX 1.0E+17 -using namespace CGAL::Three; - +namespace PMP = CGAL::Polygon_mesh_processing; +using namespace CGAL::Three; Viewer_interface* (&getActiveViewer)() = Three::activeViewer; @@ -89,6 +89,9 @@ class Display_property_plugin double gI = 1.; double bI = 0.; + double expand_radius = 0.; + double maxEdgeLength = -1.; + Color_ramp color_ramp; std::vector color_map; QPixmap legend; @@ -102,10 +105,10 @@ class Display_property_plugin MIN_VALUE, MAX_VALUE }; - - enum CurvatureType { + enum CurvatureType + { MEAN_CURVATURE, - GAUSSIAN_CURVATURE + GAUSSIAN_CURVATURE, }; public: @@ -205,6 +208,9 @@ class Display_property_plugin this, &Display_property_plugin::on_zoomToMinButton_pressed); connect(dock_widget->zoomToMaxButton, &QPushButton::pressed, this, &Display_property_plugin::on_zoomToMaxButton_pressed); + + connect(dock_widget->expandingRadiusSlider, &QSlider::valueChanged, + this, &Display_property_plugin::setExpandingRadius); } private Q_SLOTS: @@ -247,26 +253,32 @@ private Q_SLOTS: dock_widget->maxBox->setRange(0, 360); dock_widget->maxBox->setValue(0); } - else if (property_name == "Interpolated Corrected Gaussian Curvature" || - property_name == "Interpolated Corrected Mean Curvature") + else if(property_name == "Scaled Jacobian") { dock_widget->minBox->setRange(-1000, 1000); dock_widget->minBox->setValue(0); dock_widget->maxBox->setRange(-1000, 1000); dock_widget->maxBox->setValue(0); } - else if(property_name == "Scaled Jacobian") + else if(property_name == "Face Area") { dock_widget->minBox->setRange(-1000, 1000); dock_widget->minBox->setValue(0); dock_widget->maxBox->setRange(-1000, 1000); dock_widget->maxBox->setValue(0); } - else if(property_name == "Face Area") + else if (property_name == "Interpolated Corrected Mean Curvature") { - dock_widget->minBox->setRange(-1000, 1000); + dock_widget->minBox->setRange(-99999999, 99999999); dock_widget->minBox->setValue(0); - dock_widget->maxBox->setRange(-1000, 1000); + dock_widget->maxBox->setRange(-99999999, 99999999); + dock_widget->maxBox->setValue(0); + } + else if (property_name == "Interpolated Corrected Gaussian Curvature") + { + dock_widget->minBox->setRange(-99999999, 99999999); + dock_widget->minBox->setValue(0); + dock_widget->maxBox->setRange(-99999999, 99999999); dock_widget->maxBox->setValue(0); } else @@ -500,6 +512,15 @@ private Q_SLOTS: { dock_widget->setEnabled(true); disableExtremeValues(); // only available after coloring + + // Curvature property-specific slider + const std::string& property_name = dock_widget->propertyBox->currentText().toStdString(); + const bool is_curvature_property = (property_name == "Interpolated Corrected Mean Curvature" || + property_name == "Interpolated Corrected Gaussian Curvature"); + dock_widget->expandingRadiusLabel->setVisible(is_curvature_property); + dock_widget->expandingRadiusSlider->setVisible(is_curvature_property); + dock_widget->expandingRadiusLabel->setEnabled(is_curvature_property); + dock_widget->expandingRadiusSlider->setEnabled(is_curvature_property); } else // no or broken property { @@ -527,15 +548,10 @@ private Q_SLOTS: { CGAL_assertion(static_cast(dock_widget->propertyBox->count()) == property_simplex_types.size()); - const int property_index = dock_widget->propertyBox->currentIndex(); - // leave it flat if it was, otherwise set to flat+edges - if(sm_item->renderingMode() != Flat && sm_item->renderingMode() != FlatPlusEdges && property_simplex_types.at(property_index) == Property_simplex_type::FACE) + if(sm_item->renderingMode() != Flat && sm_item->renderingMode() != FlatPlusEdges) sm_item->setRenderingMode(FlatPlusEdges); - if(sm_item->renderingMode() != Gouraud && sm_item->renderingMode() != GouraudPlusEdges && property_simplex_types.at(property_index) == Property_simplex_type::VERTEX) - sm_item->setRenderingMode(GouraudPlusEdges); - const std::string& property_name = dock_widget->propertyBox->currentText().toStdString(); if(property_name == "Smallest Angle Per Face") { @@ -555,11 +571,13 @@ private Q_SLOTS: } else if(property_name == "Interpolated Corrected Mean Curvature") { - displayCurvature(sm_item, MEAN_CURVATURE); + displayInterpolatedCurvatureMeasure(sm_item, MEAN_CURVATURE); + sm_item->setRenderingMode(Gouraud); } else if(property_name == "Interpolated Corrected Gaussian Curvature") { - displayCurvature(sm_item, GAUSSIAN_CURVATURE); + displayInterpolatedCurvatureMeasure(sm_item, GAUSSIAN_CURVATURE); + sm_item->setRenderingMode(Gouraud); } else { @@ -663,8 +681,8 @@ private Q_SLOTS: removeDisplayPluginProperty(item, "f:display_plugin_largest_angle"); removeDisplayPluginProperty(item, "f:display_plugin_scaled_jacobian"); removeDisplayPluginProperty(item, "f:display_plugin_area"); - removeDisplayPluginProperty(item, "f:display_plugin_mean_curvature"); - removeDisplayPluginProperty(item, "f:display_plugin_gaussian_curvature"); + removeDisplayPluginProperty(item, "v:display_plugin_interpolated_corrected_mean_curvature"); + removeDisplayPluginProperty(item, "v:display_plugin_interpolated_corrected_Gaussian_curvature"); } void displayExtremumAnglePerFace(Scene_surface_mesh_item* sm_item, @@ -753,7 +771,7 @@ private Q_SLOTS: halfedge_descriptor local_border_h = opposite(halfedge(local_f, local_smesh), local_smesh); CGAL_assertion(is_border(local_border_h, local_smesh)); - CGAL::Polygon_mesh_processing::triangulate_faces(local_smesh); + PMP::triangulate_faces(local_smesh); double extremum_angle_in_face = ARBITRARY_DBL_MAX; halfedge_descriptor local_border_end_h = local_border_h; @@ -806,7 +824,7 @@ private Q_SLOTS: const SMesh& mesh) const { if(CGAL::is_triangle(halfedge(f, mesh), mesh)) - return CGAL::Polygon_mesh_processing::face_area(f, mesh); + return PMP::face_area(f, mesh); auto vpm = get(boost::vertex_point, mesh); @@ -822,8 +840,8 @@ private Q_SLOTS: } CGAL::Euler::add_face(local_vertices, local_smesh); - CGAL::Polygon_mesh_processing::triangulate_faces(local_smesh); - return CGAL::Polygon_mesh_processing::area(local_smesh); + PMP::triangulate_faces(local_smesh); + return PMP::area(local_smesh); } void displayArea(Scene_surface_mesh_item* sm_item) @@ -845,30 +863,113 @@ private Q_SLOTS: displaySMProperty("f:display_plugin_area", *sm); } - void displayCurvature(Scene_surface_mesh_item* sm_item, - const CurvatureType curvature_type) +private Q_SLOTS: + void setExpandingRadius() { - SMesh* sm = sm_item->face_graph(); - if(sm == nullptr) + double sliderMin = dock_widget->expandingRadiusSlider->minimum(); + double sliderMax = dock_widget->expandingRadiusSlider->maximum() - sliderMin; + double val = dock_widget->expandingRadiusSlider->value() - sliderMin; + sliderMin = 0; + + Scene_item* item = scene->item(scene->mainSelectionIndex()); + Scene_surface_mesh_item* sm_item = qobject_cast(item); + if(sm_item == nullptr) return; - bool not_initialized; - std::string tied_string = (curvature_type == MEAN_CURVATURE) ? - "v:display_plugin_mean_curvature" : "v:display_plugin_gaussian_curvature"; + SMesh& smesh = *(sm_item->face_graph()); - SMesh::Property_map vcurvature; - std::tie(vcurvature, not_initialized) = sm->add_property_map(tied_string, 0); + auto vpm = get(CGAL::vertex_point, smesh); - if (curvature_type == MEAN_CURVATURE) + // @todo use the upcoming PMP::longest_edge + if(maxEdgeLength < 0) { - CGAL::Polygon_mesh_processing::interpolated_corrected_curvatures(*sm, CGAL::parameters::vertex_mean_curvature(vcurvature)); + auto edge_range = CGAL::edges(smesh); + + if(num_edges(smesh) == 0) + { + expand_radius = 0; + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius: %1").arg(expand_radius)); + return; + } + + auto eit = std::max_element(edge_range.begin(), edge_range.end(), + [&, vpm, smesh](auto l, auto r) + { + auto res = EPICK().compare_squared_distance_3_object()( + get(vpm, source((l), smesh)), + get(vpm, target((l), smesh)), + get(vpm, source((r), smesh)), + get(vpm, target((r), smesh))); + return (res == CGAL::SMALLER); + }); + + CGAL_assertion(eit != edge_range.end()); + + maxEdgeLength = PMP::edge_length(*eit, smesh); } - else if (curvature_type == GAUSSIAN_CURVATURE) + + double outMax = 5 * maxEdgeLength, base = 1.2; + + expand_radius = (pow(base, val) - 1) * outMax / (pow(base, sliderMax) - 1); + dock_widget->expandingRadiusLabel->setText(tr("Expanding Radius: %1").arg(expand_radius)); + } + +private: + void displayInterpolatedCurvatureMeasure(Scene_surface_mesh_item* item, + CurvatureType mu_index) + { + if(mu_index != MEAN_CURVATURE && mu_index != GAUSSIAN_CURVATURE) + return; + + std::string tied_string = (mu_index == MEAN_CURVATURE) ? "v:display_plugin_interpolated_corrected_mean_curvature" + : "v:display_plugin_interpolated_corrected_Gaussian_curvature"; + + SMesh& smesh = *item->face_graph(); + + const auto vnm = smesh.property_map("v:normal_before_perturbation").first; + const bool vnm_exists = smesh.property_map("v:normal_before_perturbation").second; + + // compute once and store the value per vertex + bool non_init; + SMesh::Property_map mu_i_map; + std::tie(mu_i_map, non_init) = smesh.add_property_map(tied_string, 0); + if(non_init) { - CGAL::Polygon_mesh_processing::interpolated_corrected_curvatures(*sm, CGAL::parameters::vertex_Gaussian_curvature(vcurvature)); + if(vnm_exists) + { + if(mu_index == MEAN_CURVATURE) + { + PMP::interpolated_corrected_curvatures(smesh, + CGAL::parameters::vertex_mean_curvature_map(mu_i_map) + .ball_radius(expand_radius) + .vertex_normal_map(vnm)); + } + else + { + PMP::interpolated_corrected_curvatures(smesh, + CGAL::parameters::vertex_Gaussian_curvature_map(mu_i_map) + .ball_radius(expand_radius) + .vertex_normal_map(vnm)); + } + } + else + { + if(mu_index == MEAN_CURVATURE) + { + PMP::interpolated_corrected_curvatures(smesh, + CGAL::parameters::vertex_mean_curvature_map(mu_i_map) + .ball_radius(expand_radius)); + } + else + { + PMP::interpolated_corrected_curvatures(smesh, + CGAL::parameters::vertex_Gaussian_curvature_map(mu_i_map) + .ball_radius(expand_radius)); + } + } } - displaySMProperty(tied_string, *sm); + displaySMProperty(tied_string, smesh); } private: @@ -1027,9 +1128,9 @@ private Q_SLOTS: else if(property_name == "Face Area") zoomToSimplexWithPropertyExtremum(faces(mesh), mesh, "f:display_plugin_area", extremum); else if(property_name == "Interpolated Corrected Mean Curvature") - zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_mean_curvature", extremum); + zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_interpolated_corrected_mean_curvature", extremum); else if(property_name == "Interpolated Corrected Gaussian Curvature") - zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_gaussian_curvature", extremum); + zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, "v:display_plugin_interpolated_corrected_Gaussian_curvature", extremum); else if(property_simplex_types.at(property_index) == Property_simplex_type::VERTEX) zoomToSimplexWithPropertyExtremum(vertices(mesh), mesh, property_name, extremum); else if(property_simplex_types.at(property_index) == Property_simplex_type::FACE) @@ -1312,7 +1413,7 @@ scaled_jacobian(const face_descriptor f, for(std::size_t i=0; i Date: Thu, 16 Nov 2023 17:28:23 +0100 Subject: [PATCH 124/124] remove todo There will not be any crash as the value for new vertices is always set. However with Polyhedron since we don't reuse vertices, the map size might get large. Surface_mesh is fine --- .../include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h index a98b22de4edc..a61edc699d06 100644 --- a/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h +++ b/Polygon_mesh_processing/include/CGAL/Polygon_mesh_processing/Adaptive_sizing_field.h @@ -145,7 +145,6 @@ class Adaptive_sizing_field void calc_sizing_map(FaceGraph& face_graph , const NamedParameters& np) { - //todo ip: please check if this is good enough to store curvature typedef Principal_curvatures_and_directions Principal_curvatures; typedef typename CGAL::dynamic_vertex_property_t Vertex_curvature_tag; typedef typename boost::property_map