#pragma once #include "TriMesh.hpp" #include "CgalUtils.hpp" #include "Callbacks.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Slic3r { namespace tex2color { namespace PMP = CGAL::Polygon_mesh_processing; // Default upper bound on the number of half-edges in any single boundary cycle // that CloseBoundariesAndRepairManifoldness will attempt to triangulate. The // cost of triangulate_hole grows non-linearly with cycle length, so this caps // the worst-case per-hole work rather than the aggregate boundary size: a mesh // with many small holes is still fully repaired, while a mesh containing one // pathologically large hole skips triangulation entirely. inline constexpr std::size_t MAX_REPAIRABLE_MESH_HOLE_EDGES = 500; // Default upper bound on the aggregate number of boundary half-edges in the // mesh (summed across every boundary cycle). When the total boundary length is // excessive, even if each individual cycle is short, triangulating all of them // usually indicates a severely fragmented input (e.g. heavily damaged scans) // and rarely yields a usable result, so we skip hole closing entirely. inline constexpr std::size_t MAX_REPAIRABLE_MESH_BOUNDARY_EDGES = 5000; struct RepairSetting { // Skip triangulating a boundary cycle whose half-edge count exceeds this. std::size_t max_hole_edges = MAX_REPAIRABLE_MESH_HOLE_EDGES; // Skip hole closing entirely when the total boundary half-edge count // (summed across all cycles) exceeds this. std::size_t max_boundary_edges = MAX_REPAIRABLE_MESH_BOUNDARY_EDGES; }; struct BoundaryEdgeStats { std::size_t total_boundary_edges = 0; std::size_t max_cycle_edges = 0; std::size_t cycle_count = 0; }; // Read-only inspection of the mesh's boundary cycles. Caller is responsible for // any pre-processing (e.g. stitch_borders) needed for the count to be meaningful. inline BoundaryEdgeStats ComputeBoundaryEdgeStats(const cgalutils::CGALMesh& cgal_mesh) { using CGALMesh = cgalutils::CGALMesh; using HalfedgeDescriptor = boost::graph_traits::halfedge_descriptor; std::vector border_cycles; PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles)); BoundaryEdgeStats stats; stats.cycle_count = border_cycles.size(); for (const HalfedgeDescriptor h0 : border_cycles) { std::size_t len = 0; HalfedgeDescriptor h = h0; do { ++len; h = next(h, cgal_mesh); } while (h != h0); stats.max_cycle_edges = std::max(stats.max_cycle_edges, len); stats.total_boundary_edges += len; } return stats; } // Unconditionally close every boundary cycle of the mesh and repair non-manifold // vertices. The caller (e.g. RepairMesh) is expected to gate this call based on // boundary statistics; entering this function always triggers triangulation. inline void CloseBoundariesAndRepairManifoldness(cgalutils::CGALMesh& cgal_mesh) { using CGALMesh = cgalutils::CGALMesh; using HalfedgeDescriptor = boost::graph_traits::halfedge_descriptor; using FaceDescriptor = boost::graph_traits::face_descriptor; PMP::stitch_borders(cgal_mesh); PMP::duplicate_non_manifold_vertices(cgal_mesh); std::vector border_cycles; PMP::extract_boundary_cycles(cgal_mesh, std::back_inserter(border_cycles)); for (const HalfedgeDescriptor h : border_cycles) { std::vector patch_faces; PMP::triangulate_hole(cgal_mesh, h, std::back_inserter(patch_faces)); } PMP::remove_degenerate_faces(cgal_mesh); PMP::duplicate_non_manifold_vertices(cgal_mesh); } inline bool RepairMesh(const TriMesh& mesh, std::shared_ptr& out_mesh, AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr, const RepairSetting& setting = RepairSetting{}) { using Clock = std::chrono::steady_clock; auto elapsed_ms = [](Clock::time_point t0) { return std::chrono::duration_cast(Clock::now() - t0).count(); }; const Clock::time_point t_total = Clock::now(); // Convert TriMesh to polygon soup (point container + triangle index container) std::vector soup_points; std::vector> soup_triangles; soup_points.reserve(mesh.vertices.size()); for (const TriVertex& v : mesh.vertices) { soup_points.emplace_back(v.x(), v.y(), v.z()); } soup_triangles.reserve(mesh.indices.size()); for (const TriFace& f : mesh.indices) { soup_triangles.push_back({static_cast(f[0]), static_cast(f[1]), static_cast(f[2])}); } if (progress_callback) { progress_callback({30, "Repairing polygon soup"}); } if (cancel_callback && cancel_callback()) { return false; } { const auto t0 = Clock::now(); PMP::repair_polygon_soup(soup_points, soup_triangles); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=repair_polygon_soup took=" << elapsed_ms(t0) << " ms"; } if (progress_callback) { progress_callback({50, "Orienting polygon soup"}); } if (cancel_callback && cancel_callback()) { return false; } { const auto t0 = Clock::now(); PMP::orient_polygon_soup(soup_points, soup_triangles); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=orient_polygon_soup took=" << elapsed_ms(t0) << " ms"; } if (progress_callback) { progress_callback({70, "Converting to CGAL mesh"}); } if (cancel_callback && cancel_callback()) { return false; } cgalutils::CGALMesh cgal_mesh; { const auto t0 = Clock::now(); PMP::polygon_soup_to_polygon_mesh(soup_points, soup_triangles, cgal_mesh); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=polygon_soup_to_polygon_mesh took=" << elapsed_ms(t0) << " ms"; } { const auto t0 = Clock::now(); PMP::remove_degenerate_faces(cgal_mesh); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=remove_degenerate_faces took=" << elapsed_ms(t0) << " ms"; } if (progress_callback) { progress_callback({80, "Closing mesh boundaries"}); } if (cancel_callback && cancel_callback()) { return false; } // Stitch borders and duplicate non-manifold vertices first so that the // boundary statistics below reflect the post-stitch topology; otherwise // boundaries that would close on stitching inflate the counts and may // cause the gate to skip hole filling unnecessarily. BoundaryEdgeStats stats; { const auto t0 = Clock::now(); PMP::stitch_borders(cgal_mesh); PMP::duplicate_non_manifold_vertices(cgal_mesh); stats = ComputeBoundaryEdgeStats(cgal_mesh); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=boundary_stats took=" << elapsed_ms(t0) << " ms" << " total_boundary_edges=" << stats.total_boundary_edges << " max_cycle_edges=" << stats.max_cycle_edges << " cycle_count=" << stats.cycle_count; } const bool can_repair_holes = stats.total_boundary_edges <= setting.max_boundary_edges && stats.max_cycle_edges <= setting.max_hole_edges; if (can_repair_holes) { const auto t0 = Clock::now(); CloseBoundariesAndRepairManifoldness(cgal_mesh); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=close_boundaries took=" << elapsed_ms(t0) << " ms"; } else { BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh skip hole closing" << ", total_boundary_edges=" << stats.total_boundary_edges << " (limit=" << setting.max_boundary_edges << ")" << ", max_cycle_edges=" << stats.max_cycle_edges << " (limit=" << setting.max_hole_edges << ")" << ", cycle_count=" << stats.cycle_count; } if (progress_callback) { progress_callback({85, "Converting from CGAL mesh"}); } if (cancel_callback && cancel_callback()) { return false; } std::shared_ptr out; { const auto t0 = Clock::now(); out = std::make_shared(cgalutils::cgal_to_trimesh(cgal_mesh)); BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh stage=cgal_to_trimesh took=" << elapsed_ms(t0) << " ms"; } out_mesh = std::move(out); if (progress_callback) { progress_callback({100, "Done"}); } BOOST_LOG_TRIVIAL(info) << "TextureToColor: RepairMesh total=" << elapsed_ms(t_total) << " ms"; return true; } } // namespace tex2color } // namespace Slic3r