diff --git a/src/YgorMath.cc b/src/YgorMath.cc index 1de87bc..8f65bee 100644 --- a/src/YgorMath.cc +++ b/src/YgorMath.cc @@ -6325,6 +6325,47 @@ fv_surface_mesh::recreate_involved_face_index(void){ #endif +// Apply a surgical update to this->involved_faces. +template +void +fv_surface_mesh::apply_involved_face_index_diff(const involved_face_index_diff &diff){ + // Extend involved_faces for new vertices. + for(I i = 0; i < diff.new_vertex_count; ++i){ + this->involved_faces.emplace_back(); + } + + // Apply removals. + for(const auto &entry : diff.entries_to_remove){ + const auto v_idx = entry.first; + const auto f_idx = entry.second; + if(static_cast(v_idx) >= this->involved_faces.size()) continue; + auto &face_list = this->involved_faces[v_idx]; + auto it = std::find(face_list.begin(), face_list.end(), f_idx); + if(it != face_list.end()){ + // Swap-and-pop for O(1) erasure (order of entries is not significant). + std::swap(*it, face_list.back()); + face_list.pop_back(); + } + } + + // Apply additions. + for(const auto &entry : diff.entries_to_add){ + const auto v_idx = entry.first; + const auto f_idx = entry.second; + if(static_cast(v_idx) >= this->involved_faces.size()) continue; + this->involved_faces[v_idx].push_back(f_idx); + } + return; +} +#ifndef YGORMATH_DISABLE_ALL_SPECIALIZATIONS + template void fv_surface_mesh::apply_involved_face_index_diff(const involved_face_index_diff &); + template void fv_surface_mesh::apply_involved_face_index_diff(const involved_face_index_diff &); + + template void fv_surface_mesh::apply_involved_face_index_diff(const involved_face_index_diff &); + template void fv_surface_mesh::apply_involved_face_index_diff(const involved_face_index_diff &); +#endif + + // Re-compute this->vertex_normals from current face orientations. template void diff --git a/src/YgorMath.h b/src/YgorMath.h index 2d3a029..3ec0a7b 100644 --- a/src/YgorMath.h +++ b/src/YgorMath.h @@ -537,6 +537,34 @@ Estimate_Contour_Separation(const std::list +struct involved_face_index_diff { + // Number of new vertices added to the mesh. + // The involved_faces vector will be extended with this many empty entries. + I new_vertex_count = 0; + + // Specific (vertex_index, face_index) associations to remove. + // Each pair means: remove face_index from involved_faces[vertex_index]. + std::vector> entries_to_remove; + + // Specific (vertex_index, face_index) associations to add. + // Each pair means: add face_index to involved_faces[vertex_index]. + std::vector> entries_to_add; +}; + +//--------------------------------------------------------------------------------------------------------------------------- + //Simple, direct face-vertex list data structure representing a 3D surface mesh. Few constraints are imposed by this // data structure, and the meshes are not guaranteed to have many specific qualities. For example, multiple disconnected // meshes can be combined together without specifying whether they represent internal/external surfaces or are nested @@ -597,6 +625,16 @@ template class fv_surface_mesh { // Regenerates this->involved_faces using this->vertices and this->faces. void recreate_involved_face_index(void); + // Apply a surgical update to this->involved_faces. + // + // This is an efficient alternative to recreate_involved_face_index() when only + // a small number of faces or vertices have changed. The diff describes which + // (vertex, face) associations to remove and which to add. New vertex entries are + // appended first, then all removals are applied, then all additions. + // The resulting index is identical to what recreate_involved_face_index() would + // produce given the same mesh state. + void apply_involved_face_index_diff(const involved_face_index_diff &diff); + // Re-compute this->vertex_normals using the current face orientations. // // Each vertex normal is the area-weighted average of the face normals of all diff --git a/src/YgorMeshesRemeshing.cc b/src/YgorMeshesRemeshing.cc index 3d91133..f6fbac9 100644 --- a/src/YgorMeshesRemeshing.cc +++ b/src/YgorMeshesRemeshing.cc @@ -39,6 +39,14 @@ int64_t mesh_remesher::remesh_iteration() { return total_changes; } +template +void mesh_remesher::ensure_involved_faces_index() { + if(m_mesh.involved_faces.empty() || + m_mesh.involved_faces.size() != m_mesh.vertices.size()) { + m_mesh.recreate_involved_face_index(); + } +} + template std::set> mesh_remesher::get_all_edges() const { std::set> edges; @@ -58,17 +66,38 @@ std::set> mesh_remesher::get_all_edges() const { template std::vector mesh_remesher::get_faces_sharing_edge(I v0, I v1) const { std::vector shared_faces; - for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { - const auto &face = m_mesh.faces[f_idx]; - bool has_v0 = false, has_v1 = false; - for(const auto &v : face) { - if(v == v0) has_v0 = true; - if(v == v1) has_v1 = true; + + // Use the involved_faces index for O(k) lookup when available. + if(!m_mesh.involved_faces.empty() && + static_cast(v0) < m_mesh.involved_faces.size() && + static_cast(v1) < m_mesh.involved_faces.size()) { + // Iterate over faces incident to v0 and check if they also contain v1. + const auto &faces_v0 = m_mesh.involved_faces[v0]; + for(const auto &f_idx : faces_v0) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; + const auto &face = m_mesh.faces[f_idx]; + for(const auto &v : face) { + if(v == v1) { + shared_faces.push_back(f_idx); + break; + } + } } - if(has_v0 && has_v1) { - shared_faces.push_back(static_cast(f_idx)); + } else { + // Fallback: iterate through all faces. + for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { + const auto &face = m_mesh.faces[f_idx]; + bool has_v0 = false, has_v1 = false; + for(const auto &v : face) { + if(v == v0) has_v0 = true; + if(v == v1) has_v1 = true; + } + if(has_v0 && has_v1) { + shared_faces.push_back(static_cast(f_idx)); + } } } + return shared_faces; } @@ -87,22 +116,40 @@ I mesh_remesher::get_opposite_vertex(I face_idx, I v0, I v1) const { template I mesh_remesher::vertex_valence(I v_idx) const { std::set neighbors; - for(const auto &face : m_mesh.faces) { - bool contains_v = false; - for(const auto &v : face) { - if(v == v_idx) { - contains_v = true; - break; - } - } - if(contains_v) { + + // Use the involved_faces index for O(k) lookup when available. + if(!m_mesh.involved_faces.empty() && + static_cast(v_idx) < m_mesh.involved_faces.size()) { + const auto &incident_faces = m_mesh.involved_faces[v_idx]; + for(const auto &f_idx : incident_faces) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; + const auto &face = m_mesh.faces[f_idx]; for(const auto &v : face) { if(v != v_idx) { neighbors.insert(v); } } } + } else { + // Fallback: iterate through all faces. + for(const auto &face : m_mesh.faces) { + bool contains_v = false; + for(const auto &v : face) { + if(v == v_idx) { + contains_v = true; + break; + } + } + if(contains_v) { + for(const auto &v : face) { + if(v != v_idx) { + neighbors.insert(v); + } + } + } + } } + return static_cast(neighbors.size()); } @@ -116,6 +163,7 @@ bool mesh_remesher::is_boundary_vertex(I v_idx) const { static_cast(v_idx) < m_mesh.involved_faces.size()) { const auto &incident_faces = m_mesh.involved_faces[v_idx]; for(const auto &f_idx : incident_faces) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; const auto &face = m_mesh.faces[f_idx]; for(const auto &v : face) { if(v != v_idx) { @@ -155,23 +203,16 @@ bool mesh_remesher::is_boundary_vertex(I v_idx) const { template vec3 mesh_remesher::vertex_normal(I v_idx) const { vec3 normal(static_cast(0), static_cast(0), static_cast(0)); - - for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { - const auto &face = m_mesh.faces[f_idx]; - if(face.size() < 3) continue; - - bool contains_v = false; - for(const auto &v : face) { - if(v == v_idx) { - contains_v = true; - break; - } - } - - if(contains_v) { - // Compute face normal using triangle fan decomposition. - // Note: While the remesher is designed for triangular meshes, this method - // handles arbitrary polygons for robustness in case non-triangular faces exist. + + // Use the involved_faces index for O(k) lookup when available. + if(!m_mesh.involved_faces.empty() && + static_cast(v_idx) < m_mesh.involved_faces.size()) { + const auto &incident_faces = m_mesh.involved_faces[v_idx]; + for(const auto &f_idx : incident_faces) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; + const auto &face = m_mesh.faces[f_idx]; + if(face.size() < 3) continue; + const auto &p0 = m_mesh.vertices[face[0]]; vec3 face_normal(static_cast(0), static_cast(0), static_cast(0)); for(size_t i = 1; i + 1 < face.size(); ++i) { @@ -179,12 +220,35 @@ vec3 mesh_remesher::vertex_normal(I v_idx) const { const auto &pj = m_mesh.vertices[face[i + 1]]; face_normal += (pi - p0).Cross(pj - p0); } - // The magnitude of the accumulated cross products is 2x the polygon area, - // which serves as a weight. normal += face_normal; } + } else { + // Fallback: iterate through all faces. + for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { + const auto &face = m_mesh.faces[f_idx]; + if(face.size() < 3) continue; + + bool contains_v = false; + for(const auto &v : face) { + if(v == v_idx) { + contains_v = true; + break; + } + } + + if(contains_v) { + const auto &p0 = m_mesh.vertices[face[0]]; + vec3 face_normal(static_cast(0), static_cast(0), static_cast(0)); + for(size_t i = 1; i + 1 < face.size(); ++i) { + const auto &pi = m_mesh.vertices[face[i]]; + const auto &pj = m_mesh.vertices[face[i + 1]]; + face_normal += (pi - p0).Cross(pj - p0); + } + normal += face_normal; + } + } } - + T len = normal.length(); if(len > static_cast(1e-10)) { normal = normal / len; @@ -195,28 +259,45 @@ vec3 mesh_remesher::vertex_normal(I v_idx) const { template vec3 mesh_remesher::one_ring_centroid(I v_idx) const { std::set neighbors; - for(const auto &face : m_mesh.faces) { - bool contains_v = false; - for(const auto &v : face) { - if(v == v_idx) { - contains_v = true; - break; - } - } - if(contains_v) { + + // Use the involved_faces index for O(k) lookup when available. + if(!m_mesh.involved_faces.empty() && + static_cast(v_idx) < m_mesh.involved_faces.size()) { + const auto &incident_faces = m_mesh.involved_faces[v_idx]; + for(const auto &f_idx : incident_faces) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; + const auto &face = m_mesh.faces[f_idx]; for(const auto &v : face) { if(v != v_idx) { neighbors.insert(v); } } } + } else { + // Fallback: iterate through all faces. + for(const auto &face : m_mesh.faces) { + bool contains_v = false; + for(const auto &v : face) { + if(v == v_idx) { + contains_v = true; + break; + } + } + if(contains_v) { + for(const auto &v : face) { + if(v != v_idx) { + neighbors.insert(v); + } + } + } + } } - + vec3 centroid(static_cast(0), static_cast(0), static_cast(0)); if(neighbors.empty()) { return m_mesh.vertices[v_idx]; } - + for(const auto &n : neighbors) { centroid += m_mesh.vertices[n]; } @@ -226,11 +307,12 @@ vec3 mesh_remesher::one_ring_centroid(I v_idx) const { template bool mesh_remesher::collapse_would_invert_faces(I v_keep, I v_remove, const vec3 &new_pos) const { - // Check all faces that reference v_remove (except those that will be deleted). - for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { + // Check faces that reference v_remove (except those that will be deleted). + // Use the involved_faces index for O(k) lookup when available. + auto check_face = [&](size_t f_idx) -> bool { const auto &face = m_mesh.faces[f_idx]; // Only check triangular faces; skip non-triangular faces. - if(face.size() != 3) continue; + if(face.size() != 3) return false; // Check if this face contains v_remove but NOT v_keep (these faces will be modified). bool has_v_remove = false; @@ -266,36 +348,143 @@ bool mesh_remesher::collapse_would_invert_faces(I v_keep, I v_remove, cons return true; } } + return false; + }; + + if(!m_mesh.involved_faces.empty() && + static_cast(v_remove) < m_mesh.involved_faces.size()) { + const auto &incident_faces = m_mesh.involved_faces[v_remove]; + for(const auto &f_idx : incident_faces) { + if(static_cast(f_idx) >= m_mesh.faces.size()) continue; + if(check_face(f_idx)) return true; + } + } else { + // Fallback: iterate through all faces. + for(size_t f_idx = 0; f_idx < m_mesh.faces.size(); ++f_idx) { + if(check_face(f_idx)) return true; + } } + return false; } template void mesh_remesher::do_collapse_edge(I v_keep, I v_remove) { - // Remove faces that have both v_keep and v_remove. - auto it = m_mesh.faces.begin(); - while(it != m_mesh.faces.end()) { - bool has_keep = false, has_remove = false; - for(const auto &v : *it) { - if(v == v_keep) has_keep = true; - if(v == v_remove) has_remove = true; + involved_face_index_diff diff; + const I N = static_cast(m_mesh.faces.size()); + + // Phase 1: Identify dead faces (contain both v_keep and v_remove) + // and modified faces (contain v_remove but not v_keep). + std::vector dead_faces; + std::vector modified_faces; + + if(!m_mesh.involved_faces.empty() && + static_cast(v_remove) < m_mesh.involved_faces.size()) { + // Use the index for O(k) identification. + std::set faces_of_keep; + if(static_cast(v_keep) < m_mesh.involved_faces.size()) { + faces_of_keep.insert(m_mesh.involved_faces[v_keep].begin(), + m_mesh.involved_faces[v_keep].end()); } - if(has_keep && has_remove) { - it = m_mesh.faces.erase(it); - } else { - ++it; + for(I f : m_mesh.involved_faces[v_remove]) { + if(static_cast(f) >= m_mesh.faces.size()) continue; + if(faces_of_keep.count(f)) { + dead_faces.push_back(f); + } else { + modified_faces.push_back(f); + } + } + } else { + // Fallback: scan all faces. + for(size_t f = 0; f < m_mesh.faces.size(); ++f) { + bool has_keep = false, has_remove = false; + for(const auto &v : m_mesh.faces[f]) { + if(v == v_keep) has_keep = true; + if(v == v_remove) has_remove = true; + } + if(has_keep && has_remove) { + dead_faces.push_back(static_cast(f)); + } else if(has_remove) { + modified_faces.push_back(static_cast(f)); + } } } - - // Replace v_remove with v_keep in all remaining faces. + + // Phase 2: Compute the old_index -> new_index mapping via simulated swap-and-pop. + // This allows us to build the diff entirely against the original state. + // Swap operations are recorded for replay in Phase 4. + std::vector new_pos(N); + std::iota(new_pos.begin(), new_pos.end(), static_cast(0)); + std::vector current_face(N); + std::iota(current_face.begin(), current_face.end(), static_cast(0)); + + std::set dead_set(dead_faces.begin(), dead_faces.end()); + std::sort(dead_faces.begin(), dead_faces.end(), std::greater()); + + // Record (dead_current_pos, last_pos) pairs for Phase 4 replay. + std::vector> swap_ops; + + I sim_size = N; + for(I dead_orig : dead_faces) { + I dead_cur = new_pos[dead_orig]; + --sim_size; + if(dead_cur != sim_size) { + // The original face at the current last position moves to dead_cur. + I orig_at_last = current_face[sim_size]; + new_pos[orig_at_last] = dead_cur; + current_face[dead_cur] = orig_at_last; + swap_ops.emplace_back(dead_cur, sim_size); + } + new_pos[dead_orig] = std::numeric_limits::max(); + } + + // Phase 3: Build the diff against the original state. + // 3a. Handle dead faces: remove all vertex associations. + for(I orig : dead_set) { + for(const auto &v : m_mesh.faces[orig]) { + diff.entries_to_remove.emplace_back(v, orig); + } + } + + // 3b. Handle moved faces: update face indices for their vertices. + for(I orig = 0; orig < N; ++orig) { + if(dead_set.count(orig)) continue; + if(new_pos[orig] == orig) continue; + // This face moved from orig to new_pos[orig]. + for(const auto &v : m_mesh.faces[orig]) { + I effective_v = (v == v_remove) ? v_keep : v; + // Remove the old association using the original vertex. + diff.entries_to_remove.emplace_back(v, orig); + // Add the new association using the effective (post-replacement) vertex. + diff.entries_to_add.emplace_back(effective_v, new_pos[orig]); + } + } + + // 3c. Handle vertex replacement in non-moved, non-dead faces. + for(I mod : modified_faces) { + if(new_pos[mod] != mod) continue; // Already handled by 3b. + diff.entries_to_remove.emplace_back(v_remove, mod); + diff.entries_to_add.emplace_back(v_keep, mod); + } + + // Phase 4: Apply changes to the faces vector. + // 4a. Replay the swap-and-pop operations recorded in Phase 2. + for(const auto &op : swap_ops) { + std::swap(m_mesh.faces[op.first], m_mesh.faces[op.second]); + } + for(size_t i = 0; i < dead_set.size(); ++i) { + m_mesh.faces.pop_back(); + } + + // 4b. Replace v_remove with v_keep in all remaining faces. for(auto &face : m_mesh.faces) { for(auto &v : face) { if(v == v_remove) v = v_keep; } } - - // Invalidate the involved_faces index. - m_mesh.involved_faces.clear(); + + // Phase 5: Apply the diff to the involved_faces index. + m_mesh.apply_involved_face_index_diff(diff); } template @@ -395,8 +584,15 @@ void mesh_remesher::do_flip_edge(I face_a, I face_b, I v0, I v1, I v_opp_a fb[2] = v1; } - // Invalidate the involved_faces index. - m_mesh.involved_faces.clear(); + // Apply a surgical update to the involved_faces index. + // face_a lost v1 and gained v_opp_b. + // face_b lost v0 and gained v_opp_a. + involved_face_index_diff diff; + diff.entries_to_remove.emplace_back(v1, face_a); + diff.entries_to_add.emplace_back(v_opp_b, face_a); + diff.entries_to_remove.emplace_back(v0, face_b); + diff.entries_to_add.emplace_back(v_opp_a, face_b); + m_mesh.apply_involved_face_index_diff(diff); } //--------------------------------------------------------------------------------------------------------------------------- @@ -409,6 +605,7 @@ int64_t mesh_remesher::split_long_edges() { while(changed) { changed = false; + ensure_involved_faces_index(); auto edges = get_all_edges(); for(const auto &edge : edges) { @@ -460,6 +657,10 @@ int64_t mesh_remesher::split_long_edges() { // Split all faces that contain this edge. auto shared_faces = get_faces_sharing_edge(v0, v1); + // Build a surgical diff for the involved_faces index. + involved_face_index_diff diff; + diff.new_vertex_count = 1; // new_v + for(auto f_idx : shared_faces) { const auto face_copy = m_mesh.faces[f_idx]; // Copy since we'll modify. // Only process triangular faces. @@ -493,16 +694,29 @@ int64_t mesh_remesher::split_long_edges() { // Replace the original face with face1. m_mesh.faces[f_idx] = face1; - + // Add face2 as a new face. + I f_new = static_cast(m_mesh.faces.size()); m_mesh.faces.push_back(face2); + + // Diff: face f_idx lost one of (v0, v1) and gained new_v. + // When v0_before_v1, face1=(v0, new_v, v_opp): lost v1. + // Otherwise, face1=(v1, new_v, v_opp): lost v0. + I v_lost = v0_before_v1 ? v1 : v0; + diff.entries_to_remove.emplace_back(v_lost, f_idx); + diff.entries_to_add.emplace_back(new_v, f_idx); + + // Diff: new face f_new has all three vertices. + for(const auto &fv : face2) { + diff.entries_to_add.emplace_back(fv, f_new); + } } + // Apply the surgical update to keep the index in sync. + m_mesh.apply_involved_face_index_diff(diff); + ++splits; changed = true; - - // Invalidate index and restart to handle newly created edges. - m_mesh.involved_faces.clear(); break; // Restart the loop with updated edge set. } } @@ -521,28 +735,13 @@ int64_t mesh_remesher::collapse_short_edges() { while(changed) { changed = false; + ensure_involved_faces_index(); auto edges = get_all_edges(); for(const auto &edge : edges) { I v0 = edge.first; I v1 = edge.second; - // Skip if either vertex has been invalidated (we don't actually remove vertices, - // so check if the edge still exists in any face). - bool edge_exists = false; - for(const auto &face : m_mesh.faces) { - bool has_v0 = false, has_v1 = false; - for(const auto &v : face) { - if(v == v0) has_v0 = true; - if(v == v1) has_v1 = true; - } - if(has_v0 && has_v1) { - edge_exists = true; - break; - } - } - if(!edge_exists) continue; - const auto &p0 = m_mesh.vertices[v0]; const auto &p1 = m_mesh.vertices[v1]; T edge_len = p0.distance(p1); @@ -637,6 +836,7 @@ int64_t mesh_remesher::flip_edges_for_valence() { while(changed) { changed = false; + ensure_involved_faces_index(); auto edges = get_all_edges(); for(const auto &edge : edges) { @@ -660,19 +860,8 @@ int64_t mesh_remesher::flip_edges_for_valence() { // Check if the edge between v_opp_a and v_opp_b already exists. // If so, flipping would create a duplicate edge. - bool edge_already_exists = false; - for(const auto &face : m_mesh.faces) { - bool has_a = false, has_b = false; - for(const auto &v : face) { - if(v == v_opp_a) has_a = true; - if(v == v_opp_b) has_b = true; - } - if(has_a && has_b) { - edge_already_exists = true; - break; - } - } - if(edge_already_exists) continue; + auto opp_shared = get_faces_sharing_edge(v_opp_a, v_opp_b); + if(!opp_shared.empty()) continue; // Check if flipping would improve valence. if(!flip_improves_valence(v0, v1, v_opp_a, v_opp_b)) continue; @@ -722,6 +911,8 @@ int64_t mesh_remesher::flip_edges_for_valence() { template int64_t mesh_remesher::tangential_relaxation(T lambda) { int64_t moved = 0; + + ensure_involved_faces_index(); // Compute new positions for all non-boundary vertices. std::vector> new_positions(m_mesh.vertices.size()); @@ -730,17 +921,10 @@ int64_t mesh_remesher::tangential_relaxation(T lambda) { for(size_t v_idx = 0; v_idx < m_mesh.vertices.size(); ++v_idx) { I vi = static_cast(v_idx); - // Check if this vertex is referenced by any face. - bool is_used = false; - for(const auto &face : m_mesh.faces) { - for(const auto &v : face) { - if(v == vi) { - is_used = true; - break; - } - } - if(is_used) break; - } + // Check if this vertex is referenced by any face using the involved_faces index. + bool is_used = !m_mesh.involved_faces.empty() && + v_idx < m_mesh.involved_faces.size() && + !m_mesh.involved_faces[v_idx].empty(); if(!is_used) { new_positions[v_idx] = m_mesh.vertices[v_idx]; continue; diff --git a/tests2/YgorMeshesRemeshing.cc b/tests2/YgorMeshesRemeshing.cc index 6f2ba04..299d439 100644 --- a/tests2/YgorMeshesRemeshing.cc +++ b/tests2/YgorMeshesRemeshing.cc @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -325,3 +326,36 @@ TEST_CASE("mesh_remesher vertex attributes (normals and colours) honoured during } } + +TEST_CASE("mesh_remesher completes in bounded time on a refined mesh"){ + + SUBCASE("remeshing a refined icosahedron completes promptly"){ + // Build a refined mesh with many faces by splitting the icosahedron. + auto mesh = fv_surface_mesh_icosahedron(); + for(auto &v : mesh.vertices){ + v *= 5.0; + } + + // Pre-refine: split all edges with a small target to create many triangles. + { + mesh_remesher pre(mesh, 1.0); + pre.split_long_edges(); + } + + // Verify we actually have a non-trivial mesh (> 100 faces). + REQUIRE(mesh.faces.size() > 100); + + // Now remesh with a different target edge length and verify completion and mesh validity. + mesh_remesher remesher(mesh, 1.5); + for(int iter = 0; iter < 5; ++iter){ + remesher.remesh_iteration(); + } + REQUIRE(verify_mesh_integrity(mesh)); + + // Verify that edge lengths are tending toward the target. + double mean_len = remesher.mean_edge_length(); + REQUIRE(mean_len > 0.0); + REQUIRE(mean_len < 3.0 * 1.5); // Mean should not be wildly above target. + } +} +