Mesh repair in python

Hi,

Do someone know how to replicate the mesh repair wizard into a python script? In other words, I would like to repair a broken (non-manifold, faces intersections, etc.) mesh in python, without any manual input from the ui/user.
Thank you,

Alexandre

Hi @Alexandre_Filiatraul,

All of the tools used by the MeshRepair command can be found on the Mesh class in RhinoCommon.

If you can’t find what you are looking for, @Trav can assist.

– Dale

Thanks Dale,

@Trav, Can you help me finding function calls to remove non-manifold edges, degenerate faces and intersecting faces? In the Mesh class pointed above, I dont find python functions which accomplish that; I only see c# and VB functions.
Thank you.

Alexandre

I have a nice Python script that does this with an easy to use GUI based on Eto Forms. I will post it when I get back to the office in an hour.

Regards,
Terry.

@Alexandre_Filiatraul

Extracting Non-manifolds = Mesh.ExtractNonManifoldEdges(true)

Degenerate faces = Mesh.Faces.RemoveZeroAreaFaces()

Intersections are calculated by Mesh.Faces.GetClashingFacePairs() but not removed.

Thank you!

@Alexandre_Filiatraul,

EDIT: Below is the script. It was updated on 4 Nov. 2019 to be compatible with the DLL posted below on this same date.

TrimMeshForDD.py (349.6 KB)

  1. Before you run it, you need to have one mesh visible in the Top View.
  2. When you run the script, it will create layers and put your starting mesh on layer Start Mesh to preserve the original.
  3. It will copy the Start Mesh to the Normal layer which is used by the script.
  4. The GUI looks like this with a mesh on the left and the Layers panel on the right where you see that the Normal layer is On.
  5. To clean your mesh, click on the Clean Mesh button. Then the form should look like this:
  6. To select cleaning options, click on the Show Options button which brings up the options:
  7. Notice that the Show Details button is checked. This will make the script write dozens of lines to the Command Window showing the progress and details of the cleaning operations. If you un-check this button, then only ERRORS and major details will be shown.
  8. Also notice that the Tooltips button is checked. When you hover the mouse pointer over a field on the form, it will show details. Un-check this button if you find this too distracting.
  9. I selected the type of cleanup operations you are interested in:

    and then pushed the Clean Mesh button.
  10. The cleaned mesh is put on layer Cleaned.
  11. ERROR’s or problems are highlighted with a Red point for Duplicate Faces and Blue point for Overlapping Faces:
  12. In the Command Window I got these messages:
  13. The script can also do other operations: Split the mesh, make a Hole in, create a Pit in it and Split it. The details for running these operations can be discovered using the ToolTips or I can create more details like the above if you like.

I developed the Python script on a Windows machine but use Eto Forms so it may work on a Mac. The DLL mentioned in the WARNING message is not needed for your mesh cleaning operations. But if you want it for speeding up Trim/Hole/Pit/Split operations a bit and speeding up computing the mesh Volume a lot, let me know and I can post a link to the DLL which will only run on a Windows machine.

You can customize the script to change the details of the cleaning operations by modifying this part of the code between lines 4450-4647:

    def cleanup_mesh(meshGeoS, smeshGeo, op, cleanup_options):
    	"""
    	Check and fix mesh to help ensure it is good by using these selectable options:
    	1. Removing disjoint pieces.
    	2. Removing degenerate faces.
    	3. Removing duplicate faces.
    	4. Removing 1 overlapping face in each pair of overlapping faces.
    	5. Removing non-manifold edges.
    	6. Unifying vertex normals and then creating face normals.
    	7. Removing identical vertices.
    	8. Compacting the mesh.
    	Parameters:
    		meshGeo: Main trimmed mesh.
    		smeshGeo: If splitting mesh, smaller, interior trimmed mesh.
    		op: This is 'Trim', 'Hole', 'Pit' or 'Split' which indicates the operation to be performed.
    			'Trim': The mesh outside the boundary curve is removed.
    			'Hole': The mesh inside the boundary curve is removed.
    			'Pit': The Z of all vertices inside the boundary curve are moved to a specified absolute or relative depth
    				and sides are added to connect the main mesh to the mesh in the pit.
    			'Split': The mesh is split by the boundary curve.
    		cleanup_options: List of boolean parameters for controlling cleanup operations on the mesh:
    			chk_disjoint: Remove disjoint pieces in the mesh.  The largest piece is kept.
    			chk_degen_faces: Remove degenerate faces in the mesh.
    			chk_dup_faces: Remove duplicate faces in the mesh.
    			chk_overlap_faces: Remove overlapping faces in the mesh. NOTE: This can leave holes in the mesh.
    			chk_manifold: Remove non-manifold edges.
    			chk_normals: Compute and unifty vertex normals and compute face normals.
    			chk_identical: Combine identical vertices.
    			compact_mesh: Compact the mesh.
    	Returns:
    		meshGeo: Main trimmed mesh after cleanup operations.
    		smeshGeo: If splitting mesh, smaller, interior trimmed mesh after cleanup operations.
    	"""
    	time1 = time()
    	# Get cleanup options.
    	chk_disjoint, chk_degen_faces, chk_dup_faces, chk_overlap_faces,\
    		chk_manifold, chk_normals, chk_identical, compact_mesh = cleanup_options
    	# Make list for holding meshGeos after cleaning.
    	clean_meshGeos = []
    	# Set message used in printout of results below.
    	msg = 'that'
    	if op == 'Hole': msg = 'with hole that'
    	# Initialize message for ordinary case with just a trimmed mesh.
    	msg1 = 'in trimmed mesh'
    	# Put meshGeo on list of meshes to be cleaned.
    	meshGeos = [meshGeoS]
    	# If smeshGeo exists, the mesh is being split into 2 meshes, meshGeo and smeshGeo so add smeshGeo to meshGeos list.
    	if smeshGeo:
    		meshGeos.append(smeshGeo)
    	i = 0
    	for meshGeo in meshGeos:
    		# Create duplicate geometry to fall back to if cleanup operation creates invalid mesh.
    		backup_meshGeo = meshGeo.Duplicate()
    		faces = meshGeo.Faces
    		if len(meshGeos) == 2:
    			if talk:
    				if i == 0: print '    Checking mesh that is outside the boundary curve.'
    				elif i == 1: print '    Checking mesh that is inside the boundary curve.'
    				i += 1
    		# Remove disjoint pieces in trimmed mesh.
    		if chk_disjoint:
    			timea = time()
    			pieces = meshGeo.SplitDisjointPieces()
    			if len(pieces) <= 1:
    				if talk: print '    Mesh has {0} piece with {1:,} faces so it is not disjoint determined in {2:.4f} sec'\
    					.format(len(pieces), meshGeo.Faces.Count, time() - timea)
    			# If there are disjoint pieces, keep the largest one.
    			if len(pieces) > 1:
    				# Make list of bounding box 2D-area and piece and then sort by area to get largest piece.
    				area_index = []
    				for piece in pieces:
    					bbox = piece.GetBoundingBox(XYplane0)
    					xmin, xmax, ymin, ymax = bbox.Min.X, bbox.Max.X, bbox.Min.Y, bbox.Max.Y
    					area = (xmax-xmin)*(ymax-ymin)
    					# Make list of [piece_area, piece]
    					area_index.append([area, piece])
    				# Sort list by area to put largest mesh first.
    				area_index.sort(key = getKey, reverse = True)
    				# Get largest mesh.
    				meshGeo = area_index[0][1]
    				# Get list of other pieces
    				pieces_removed = [piece for area,piece in area_index[1:]]
    				if talk:
    					print '    Time to find {0} disjoint pieces and pick largest with {1:,} faces, delete {2} pieces and leave mesh {3} IsValid = {4} is {5:.4f} sec'\
    						.format(len(pieces), meshGeo.Faces.Count, len(pieces_removed), msg, meshGeo.IsValid, time() - timea)
    		# Remove degenerate faces.
    		if chk_degen_faces:
    			timea = time()
    			degenerates = meshGeo.Faces.CullDegenerateFaces()
    			if degenerates:
    				if talk: print '    Removed {0} degenerate faces leaving {1:,} faces in {2:.4f} sec'\
    					.format(degenerates, meshGeo.Faces.Count, time() - timea)
    			else:
    				if talk: print '    Time to check for degenerate faces = {0:.4f} sec'.format(time() - timea)
    		# Remove duplicate faces.
    		if chk_dup_faces:
    			timea = time()
    			duplicates = meshGeo.Faces.ExtractDuplicateFaces()
    			if duplicates:
    				if talk: print '    Removed {0} duplicate faces (marked in red) leaving {1:,} faces in {2:.4f} sec'\
    					.format(duplicates.Faces.Count, meshGeo.Faces.Count, time() - timea)
    				# The trimming code should not have generated duplicate faces so mark their centers with red point.
    				for i in range(duplicates.Faces.Count):
    					pt = duplicates.Faces.GetFaceCenter(i)
    					f_index, npt = meshGeo.ClosestPoint(pt, 0.0)
    					print '      Face index = {} location = {} marked in red'.format(f_index,pt)
    					SP(pt, red)
    			else:
    				if talk: print '    Time to check for duplicate faces = {0:.4f} sec'.format(time() - timea)
    		if chk_overlap_faces:
    		# Remove overlapping faces.
    			timea = time()
    			overlaps = meshGeo.Faces.GetClashingFacePairs(-1)
    			if len(overlaps) > 0:
    				faces_to_delete = []
    				# Get overlapping faces.
    				for pair in overlaps:
    					color1 = red
    					for face in pair:
    						if color1 == blue: faces_to_delete.append(face)
    						SP(faces.GetFaceCenter(face),color1)
    						color1 = blue
    						# Breaking now results in removing one face in each pair.
    						#break
    				# Delete overlapping faces.
    				meshGeo.Faces.DeleteFaces(faces_to_delete, True)
    				if talk: print '    Time to find {0} intersecting faces and removed 1 face in each pair leaving mesh that is valid = {1} is {2:.4f} sec\nNOTE: This may leave holes in the mesh.'\
    					.format(len(overlaps), meshGeo.IsValid, time() - timea)
    			else:
    				if talk: print '    Time to check for overlapping faces = {0:.4f} sec'.format(time() - timea)
    		# Remove non-manifold edges.
    		if chk_manifold:
    			timea = time()
    			is_manifold = meshGeo.IsManifold(True)
    			if not is_manifold[0]:
    				if talk: print '    Mesh is not manifold = {0} so calling ExtractNonManifoldEdges to try and fix mesh with {1:,} faces.'\
    					.format(is_manifold, meshGeo.Faces.Count)
    				meshMani = meshGeo.ExtractNonManifoldEdges(False)
    				if talk:
    					if meshMani: print '    Number of non-manifold parts removed = ', meshMani.Faces.Count
    					print '    Is_Manifold now returns = {0} and mesh has {1:,} faces'.format(meshGeo.IsManifold(True), meshGeo.Faces.Count)	
    					mani = doc.Objects.AddMesh(meshMani)
    					setupLayer('Non-Manifold',None,'3D Model')
    					setLayer([mani], 'Non-Manifold')
    			else:
    				if talk: print '    Time to check if mesh IsManifold = {0:.4f} sec'.format(time() - timea)
    		# Combine identical vertices.  This must be done before the Normals operations below.
    		if chk_identical:
    			timea = time()
    			vertices = meshGeo.Vertices
    			vb = vertices.Count
    			# Merge identical vertices ignoring vertex normals but taking into account textures, colors and principal curvatures.
    			rc = vertices.CombineIdentical(True, False)
    			if talk:
    				if rc:
    					if not meshGeo.IsValid:
    						# Try compacting mesh if it is invalid.
    						meshGeo.Compact()
    						if not meshGeo.IsValid:
    							# If mesh is still invalid, use the backup.
    							meshGeo = backup_meshGeo
    							print '    Could not remove {0:,} identical vertices without making mesh invalid so this cleanup step was skipped.'.format(vb - vertices.Count)
    						else: print '    Found and removed {0:,} identical vertices leaving {1:,} vertices and {2:,} faces with mesh valid = {3} in {4:.4f} sec'.format(vb - vertices.Count, vertices.Count, meshGeo.Faces.Count, meshGeo.IsValid, time() - timea)
    					else: print '    Found and removed {0:,} identical vertices leaving {1:,} vertices and {2:,} faces with mesh valid = {3} in {4:.4f} sec'.format(vb - vertices.Count, vertices.Count, meshGeo.Faces.Count, meshGeo.IsValid, time() - timea)
    				else: print '    Mesh has no identical vertices found in {0:.4f} sec'.format(time() - timea)
    		# Compute and unifty vertex normals and compute face normals.
    		if chk_normals:
    			timea = time()
    			# Dale's suggestion was not enough.
    			# Remove any existing face and vertex normals.
    			#meshGeo.FaceNormals.Clear()
    			#meshGeo.Normals.Clear()
    			# Compute them both.
    			#meshGeo.Normals.ComputeNormals() # computes both
    			#
    			# Update face Normals. Not sure if this is needed.
    			meshGeo.FaceNormals.ComputeFaceNormals()
    			# Unify face normals by rearranging the mesh face winding and face normals to make them all consistent.  
    			meshGeo.UnifyNormals(False)
    			# Recompute vertex normals after unifying face normals.  Recommended in Rhino description of Unify Normals in script editor.
    			meshGeo.Normals.ComputeNormals()
    			if talk: print '    Time to compute & unify face normals plus compute vertex normals = {0:.4f} sec'.format(time() - timea) 
    		# Compact the trimmed mesh.
    		if compact_mesh:
    			timea = time()
    			mem_before = meshGeo.MemoryEstimate()
    			mesh_compacted = meshGeo.Compact()
    			mem_after = meshGeo.MemoryEstimate()
    			if talk:
    				if mesh_compacted and meshGeo.IsValid: print '    Time to compact trimmed mesh to {0:,.0f} MB which saved {1:,.0f} MB = {2:.4f} sec'\
    					.format(1e-6*mem_after, 1e-6*(mem_before-mem_after), time() - timea)
    				else: print '    WARNING: Trimmed mesh could not be compacted {} or trimmed mesh is not valid = {} after compacting.'\
    					.format(mesh_compacted, not meshGeo.IsValid)
    		clean_meshGeos.append(meshGeo)
    	if talk and time()-time1>0.1: print 'Time to cleanup mesh = {0:.4f} sec'.format(time() - time1)
    	meshGeo = clean_meshGeos[0]
    	if len(clean_meshGeos) == 2: smeshGeo = clean_meshGeos[1]
    	return meshGeo, smeshGeo

Let me know if you have any questions or would like changes or enhancements. I am retired and like working on my Python scripts if they can be of use to others. So do not be shy about asking. I have added 100’s of lines of new code to help Rhino users on the Forum. The code is fully documented and covers the basics of all operations so reading thru it will be of great help if you want to make changes.

Regards,
Terry.

4 Likes

Hello @Terry_Chappell

Thank you for your answer, very appreciated!

I’m currently triying to use it, to repair some broken mesh. For the moment, I’m stuck at line 4548:

duplicates = meshGeo.Faces.ExtractDuplicateFaces()

"Message: ‘MeshFaceList’ object has no attribute ‘ExtractDuplicateFaces’ "

Do you know where I can find the definition of Faces.ExtractDuplicateFaces() ? I suspect that some other similar functions (GetClashingFacePairs? ExtractNonManifoldEdges?) might also be unknown to my python interpreter.

About the Windows .dll, I’m definitely interested! What is the procedure to use it? Simply get a copy and point to it, at line 2 of the python file?

Thanks again!

Alexandre

Alexander,

Thanks for you note. I will look into the failure you are seeing when you include the Duplicate option.
Then I will find a DLL that works with your version of the script (which I have continued to improve).
And yes it should be simple to use:

  1. Download from link provided (it cannot be attached to Forum post as .dll is an illegal extension).
  2. Put it somewhere (you could put it in location similar to that show in line 2).
  3. Run the script. It should be autoloaded, if not a WARNING message will appear.

More soon.

Regards,
Terry.

Alexander,

I cannot reproduce the error you are seeing. Are you using Rhino 6 like me? Or are you using Rhino 7 WIP? Or Rhino 5?

I selected Degenerate and Duplicate checks on the form:


and then got the result below for one of my meshes.

Note that the outline of Rhinocommon procedures on the left shows CullDegenerateFaces and ExtractDuplicateFaces are called the same way.

Did you have both Degenerate and Duplicate checked for your cleaning run? If not try this combination to see if both fail when trying to use meshGeo.Faces.

Can you attach your mesh if it is small enough, or send me a link so I can try debugging it?

Regards,
Terry.

Hello Terry,

I’m currently using Rhino 5. We’re supposed to move soon to Rhino 6. I guess this would be the answer?

Alexandre

Alexandre,

Yes that is the problem. Rhino 5 does not support several of the procedures used in the script.

When you have moved to Rhino 6, the following Python script + DLL should work together.

TrimMeshForDD.py (349.6 KB)

The prior posting of the TrimMeshForDD.py file has been updated to be compatible with this DLL.

Regards,
Terry.