Mesh repair
Overview
A mesh you build yourself is clean by construction. A mesh you import — from a 3D scanner, an STL downloaded from the internet, a point cloud reconstructed by another tool — usually is not. Scanned meshes arrive with duplicated vertices, degenerate or duplicate faces, inconsistent winding, holes, and stray fragments.
Before such a mesh can be simulated, printed, or even displayed reliably, it has to be repaired. Rasmah's repair tools diagnose what is wrong and then fix it, one discrete pass at a time.
Diagnosing a mesh
The first step is to find out what is wrong. diagnose counts every class of defect and reports two summary flags — watertight and manifold:
Rasmah.MeshDoctor.MeshDiagnosis — Type
MeshDiagnosisA report of a triangle mesh's defects (counts and flags).
Fields
n_vertices/n_faces: vertex and face counts.n_degenerate_faces/n_duplicate_faces: degenerate and duplicate face counts.n_unreferenced_vertices/n_duplicate_vertices: orphan and near-coincident vertex counts.n_boundary_edges/n_nonmanifold_edges/n_nonmanifold_vertices: boundary and non-manifold counts.n_connected_components: number of connected components.area: total surface area.watertight/manifold: whether the mesh is watertight/manifold.
Rasmah.MeshDoctor.diagnose — Function
diagnose(m::TriangleMesh; tol = 1e-12) -> MeshDiagnosisReport the defects of a triangle mesh (tol the vertex-coincidence / degenerate-area tolerance).
diagnose(surface_mesh(sphere(1.0)))MeshDiagnosis(vertices=642, faces=1280, watertight=true, manifold=true, degenerate=0, duplicate_faces=0, boundary_edges=0, nonmanifold_edges=0, nonmanifold_vertices=0, components=1)A mesh Rasmah builds itself is clean: every edge is used exactly twice, so the surface is watertight and manifold with no boundary.
Making a hole
To see repair do something useful, punch a hole in a clean sphere by dropping the faces near the top:
m = surface_mesh(sphere(1.0))
cz = [sum(m.vertices[3, m.faces[:, j]]) / 3 for j in 1:size(m.faces, 2)]
holey = TriangleMesh(m.vertices, m.faces[:, [j for j in 1:size(m.faces, 2) if cz[j] < 0.8]])TriangleMesh{Matrix{Float64}, Matrix{Int64}, Nothing}([-0.5257311121191336 0.5257311121191336 … 0.912982492932299 0.912982492932299; 0.85065080835204 0.85065080835204 … 0.3996070517018512 0.3996070517018512; 0.0 0.0 … -0.08232358003196016 0.08232358003196016], [1 43 … 160 641; 163 164 … 640 642; 165 163 … 642 640], nothing)The hole shows up as a loop of boundary edges — each edge now used by only one triangle — and the watertight flag flips off:
diagnose(holey)MeshDiagnosis(vertices=642, faces=1148, watertight=false, manifold=true, degenerate=0, duplicate_faces=0, boundary_edges=36, nonmanifold_edges=0, nonmanifold_vertices=0, components=1)Filling holes
fill_holes caps each boundary loop with new triangles, closing the surface again:
Rasmah.MeshDoctor.fill_holes — Function
fill_holes(m::TriangleMesh; max_hole_size = typemax(Int), method = :earclip, refine = false, smooth_iters = 20) -> TriangleMeshFill boundary holes (loops of boundary edges) with at most max_hole_size edges.
diagnose(fill_holes(holey))MeshDiagnosis(vertices=642, faces=1182, watertight=true, manifold=true, degenerate=0, duplicate_faces=0, boundary_edges=0, nonmanifold_edges=0, nonmanifold_vertices=0, components=1)The boundary_edges count returns to 0 and the mesh is watertight once more.
The standard repair pipeline
For a mesh with several problems at once, clean_mesh runs the passes in the right order — weld, drop degenerate faces, drop duplicate faces, orient, compact:
Rasmah.MeshDoctor.clean_mesh — Function
clean_mesh(m::TriangleMesh; tol = 1e-12, orient = true, remove_small = false, min_vertices = 0, min_area = 0.0, fill = false, max_hole_size = typemax(Int)) -> TriangleMeshApply the standard repair pipeline: weld, drop degenerate, drop duplicate, orient, (optionally) drop small components and fill holes, then compact.
diagnose(clean_mesh(holey))MeshDiagnosis(vertices=593, faces=1148, watertight=false, manifold=true, degenerate=0, duplicate_faces=0, boundary_edges=36, nonmanifold_edges=0, nonmanifold_vertices=0, components=1)Orienting a mesh outward
A mesh's faces must wind consistently outward for lighting, booleans, and volume sign to work. orient_outward fixes the winding of every connected component:
Rasmah.MeshDoctor.orient_outward — Function
orient_outward(m::TriangleMesh) -> TriangleMeshMake each connected component of m wound outward (relative to the enclosed solid), not merely consistent. Closed components use the divergence-theorem signed volume (negative volume = inward), and open/ambiguous components are decided by a generalized-winding-number vote just off each face. Returns a new mesh with corrected face windings.
vtk(orient_outward(fill_holes(holey)))Next steps
Repair is the last stop before a mesh is usable. The opposite direction — going from raw points (not a mesh at all) to a mesh — is covered in From points to mesh.
