@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)
- Before you run it, you need to have one mesh visible in the Top View.
- When you run the script, it will create layers and put your starting mesh on layer Start Mesh to preserve the original.
- It will copy the Start Mesh to the Normal layer which is used by the script.
- 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.
- To clean your mesh, click on the Clean Mesh button. Then the form should look like this:
- To select cleaning options, click on the Show Options button which brings up the options:
- 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.
- 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.
- I selected the type of cleanup operations you are interested in:
and then pushed the Clean Mesh button.
- The cleaned mesh is put on layer Cleaned.
- ERROR’s or problems are highlighted with a Red point for Duplicate Faces and Blue point for Overlapping Faces:
- In the Command Window I got these messages:
- 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.