From 4b0e8b6921448e9c0120bd41ced162b7f4ffe2f1 Mon Sep 17 00:00:00 2001 From: IasonManolas Date: Mon, 3 Aug 2026 09:04:48 +0300 Subject: [PATCH 1/4] collect flip chord-test ring apices once instead of re-circulating facets find_best_flip_to_improve_dh circulated the facets around an edge once to enumerate its apices, then re-circulated them again for every apex to test for chords. The second circulation always re-derives the same ring, so collect it once into ring_apices and index the chord test on it directly. --- .../internal/flip_edges.h | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h index ed22929e6086..78bd3259b0db 100644 --- a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h +++ b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h @@ -569,23 +569,36 @@ void find_best_flip_to_improve_dh(C3t3& c3t3, Facet_circulator curr_fcirc = tr.incident_facets(edge); Facet_circulator curr_fdone = curr_fcirc; - //Only keep the possible flips - std::vector opposite_vertices; - int nb_cells_around_edge = 0; + //Collect the vertex opposite to the edge in each facet around it, in + //circulation order. The chord test below used to re-circulate the facets + //around the edge once per such vertex to enumerate the other apices; those + //are the same vertices collected here, so the tests can be indexed on the + //ring instead. is_edge_uv is read-only, so stopping at the first chord + //found yields the same verdict as running the ring to its end. + boost::container::small_vector ring_apices; do { - Vertex_handle vh; - //Get the ids of the opposite vertices + //Get the id of the opposite vertex for (int i = 0; i < 3; ++i) { Vertex_handle curr_vertex = curr_fcirc->first->vertex( indices(curr_fcirc->second, i)); if (curr_vertex != vh0 && curr_vertex != vh1) { - vh = curr_vertex; + ring_apices.push_back(curr_vertex); break; } } + } + while (++curr_fcirc != curr_fdone); + + //Only keep the possible flips + std::vector opposite_vertices; + int nb_cells_around_edge = 0; + const int n_apices = static_cast(ring_apices.size()); + for (int p = 0; p < n_apices; ++p) + { + const Vertex_handle vh = ring_apices[p]; if(tr.is_infinite(vh)) continue; @@ -594,37 +607,23 @@ void find_best_flip_to_improve_dh(C3t3& c3t3, if (o_inc_vh.empty()) tr.incident_cells(vh, std::back_inserter(o_inc_vh)); - Facet_circulator facet_circulator = curr_fcirc; - Facet_circulator facet_done = curr_fcirc; - - facet_done--; - facet_circulator++; - facet_circulator++; + //a chord is an edge joining vh to an apex that is not one of its two + //neighbors on the ring (positions p-1 and p+1) bool is_edge = false; - do + for (int j = p + 2; j <= p + n_apices - 2; ++j) { - //Get the ids of the opposite vertices - for (int i = 0; i < 3; ++i) + if (is_edge_uv(vh, ring_apices[j % n_apices], o_inc_vh)) { - Vertex_handle curr_vertex = facet_circulator->first->vertex( - indices(facet_circulator->second, i)); - if (curr_vertex != vh0 && curr_vertex != vh1) - { - if (is_edge_uv(vh, curr_vertex, o_inc_vh)) - { - is_edge = true; - break; - } - } + is_edge = true; + break; } - } while (++facet_circulator != facet_done); + } if (!is_edge) opposite_vertices.push_back(vh); nb_cells_around_edge++; } - while (++curr_fcirc != curr_fdone); if (nb_cells_around_edge < 4) return; From d63c69b12eb3aba7052f928661be41817d1d43f4 Mon Sep 17 00:00:00 2001 From: IasonManolas Date: Mon, 3 Aug 2026 09:54:43 +0300 Subject: [PATCH 2/4] flip apex vertex set: unordered_set -> small_vector find_best_flip built two unordered_set (heap allocation plus hashing) to hold the handful of always-distinct ring apices around a flip candidate edge, only ever scanned for size() and iterated. A small_vector with a linear membership check holds the same set inline. flip_3_to_2, the only consumer, does not depend on iteration order: its is_facet check is symmetric in its three arguments, and vh2/vh3 are each picked by testing individually against ch0/ch1. --- .../internal/flip_edges.h | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h index 78bd3259b0db..4703de5edaf5 100644 --- a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h +++ b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/flip_edges.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -88,7 +89,7 @@ void update_c3t3_facets(C3t3& c3t3, template Sliver_removal_result flip_3_to_2(typename C3t3::Edge& edge, C3t3& c3t3, - const std::vector& vertices_around_edge, + const boost::container::small_vector& vertices_around_edge, const Flip_Criterion& criterion, IncCellsVectorMap& inc_cells, CellSelector& cell_selector) @@ -1096,8 +1097,12 @@ Sliver_removal_result find_best_flip(typename C3t3::Edge& edge, Facet_circulator circ = tr.incident_facets(edge); Facet_circulator done = circ; - //Identify the vertices around this edge - std::unordered_set vertices_around_edge; + //Identify the vertices around this edge. The ring of apices around an edge + //holds a handful of distinct vertices, so they are kept inline and scanned + //rather than hashed. flip_3_to_2 does not depend on their order : it picks + //vh2/vh3 by testing each vertex against ch0/ch1 individually, and is_facet + //is symmetric in its three vertices. + boost::container::small_vector vertices_around_edge; bool boundary_edge = false; bool hull_edge = false; @@ -1111,7 +1116,9 @@ Sliver_removal_result find_best_flip(typename C3t3::Edge& edge, Vertex_handle vi = circ->first->vertex(indices(circ->second, i)); if (vi != v0 && vi != v1) { - vertices_around_edge.insert(vi); + if (std::find(vertices_around_edge.begin(), vertices_around_edge.end(), vi) + == vertices_around_edge.end()) + vertices_around_edge.push_back(vi); if ( circ->first->subdomain_index() != circ->first->neighbor(circ->second)->subdomain_index()) @@ -1142,9 +1149,7 @@ Sliver_removal_result find_best_flip(typename C3t3::Edge& edge, { if (!boundary_edge && !hull_edge) { - std::vector vertices; - vertices.insert(vertices.end(), vertices_around_edge.begin(), vertices_around_edge.end()); - res = flip_3_to_2(edge, c3t3, vertices, criterion, inc_cells, cell_selector); + res = flip_3_to_2(edge, c3t3, vertices_around_edge, criterion, inc_cells, cell_selector); } } else From f9af46970beb67b41a9a9a92f76749b2e1f6086c Mon Sep 17 00:00:00 2001 From: IasonManolas Date: Mon, 3 Aug 2026 09:54:54 +0300 Subject: [PATCH 3/4] topology_test: check complex membership before the subdomain star walk For each boundary-facet apex vi, topology_test ran nb_incident_subdomains(vi) (a full cell-star walk) before is_edge_in_complex(v0,vi) and (v1,vi), each of which walks the star again just to reach a single bimap lookup. vi shares its facet with v0 and v1, so both edges exist in the triangulation by construction: the star walks inside is_edge_in_complex always succeed and exist only to reach c3t3.is_in_complex(edge), which is what c3t3.is_in_complex(v0,vi) computes directly. Reordering the three (now pure) predicates so the two cheap complex-membership lookups run first leaves the conjunction's result unchanged. --- .../internal/tetrahedral_remeshing_helpers.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h index 621f2dc2c5a3..f7093df4abac 100644 --- a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h +++ b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h @@ -1308,10 +1308,15 @@ bool topology_test(const typename C3t3::Edge& edge, for (int i = 1; i < 4; i++) { Vertex_handle vi = f.first->vertex((f.second + i) % 4); - if (vi != v0 && vi != v1 && nb_incident_subdomains(vi, c3t3) > 1) + if (vi != v0 && vi != v1) { - if (is_edge_in_complex(v0, vi, c3t3) - && is_edge_in_complex(v1, vi, c3t3)) + //(v0,vi) and (v1,vi) are edges of f, so testing them for the + //complex needs no is_edge() star walk, and feature edges are rare + //enough that the subdomain star walk is skipped almost always. + //The three tests are pure, so the conjunction is unchanged. + if (c3t3.is_in_complex(v0, vi) + && c3t3.is_in_complex(v1, vi) + && nb_incident_subdomains(vi, c3t3) > 1) return false; } } From d86ad300141c9723dcd71aa979b4bc62da3be98d Mon Sep 17 00:00:00 2001 From: IasonManolas Date: Wed, 12 Aug 2026 13:25:59 +0300 Subject: [PATCH 4/4] topology_test: stop counting all the subdomains around a vertex The reordered guard only ever asks whether a second subdomain index exists around vi, never how many there are, so the star walk it ends with can stop at the first one it finds instead of counting the whole star. Written for #9590 and moved here: topology_test is its only caller, and #9590's copy of it conflicted with the reordering this branch applies to the same line. --- .../internal/tetrahedral_remeshing_helpers.h | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h index 30b4a4b46b4c..56074da2274a 100644 --- a/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h +++ b/Tetrahedral_remeshing/include/CGAL/Tetrahedral_remeshing/internal/tetrahedral_remeshing_helpers.h @@ -976,6 +976,63 @@ std::size_t nb_incident_subdomains(const typename C3t3::Vertex_handle v, return indices.size(); } +// `nb_incident_subdomains(v, c3t3) > 1`, without counting the whole star. The +// traversal is the one `TDS_3::incident_cells_3()` performs - from v's cell, +// across the facets that contain v, marking cells as it goes - so it sees the +// same cells in the same order, and stops as soon as a second index appears. +// The marks are the TDS's own conflict flags and are cleared before +// returning, so this must not be called from inside another marking +// traversal; topology_test, which calls it, is not. +template +bool has_several_incident_subdomains(const typename C3t3::Vertex_handle v, + const C3t3& c3t3) +{ + typedef typename C3t3::Triangulation::Cell_handle Cell_handle; + CGAL_USE(c3t3); + + const Cell_handle start = v->cell(); + const auto si0 = start->subdomain_index(); + + boost::container::small_vector marked; + boost::container::small_vector to_visit; + + start->tds_data().mark_in_conflict(); + marked.push_back(start); + to_visit.push_back(start); + + bool several = false; + while (!to_visit.empty()) + { + const Cell_handle c = to_visit.back(); + to_visit.pop_back(); + + if (c->subdomain_index() != si0) + { + several = true; + break; + } + + for (int i = 0; i < 4; ++i) + { + if (c->vertex(i) == v) + continue; + + const Cell_handle n = c->neighbor(i); + if (!n->tds_data().is_clear()) + continue; + + n->tds_data().mark_in_conflict(); + marked.push_back(n); + to_visit.push_back(n); + } + } + + for (const Cell_handle c : marked) + c->tds_data().clear(); + + return several; +} + template std::size_t nb_incident_subdomains(const typename C3t3::Edge& e, const C3t3& c3t3) @@ -1320,7 +1377,7 @@ bool topology_test(const typename C3t3::Edge& edge, //The three tests are pure, so the conjunction is unchanged. if (c3t3.is_in_complex(v0, vi) && c3t3.is_in_complex(v1, vi) - && nb_incident_subdomains(vi, c3t3) > 1) + && has_several_incident_subdomains(vi, c3t3)) return false; } }