Patch T41093: Cleanup non-manifold
by Caretdashcaret with own edits
This commit is contained in:
parent
544a5a6d80
commit
a88a2e6460
Notes:
blender-bot
2023-02-14 20:05:22 +01:00
Referenced by issue #41093, Add a new cleanup to 3D print tools
|
@ -133,6 +133,7 @@ classes = (
|
|||
operators.Print3DCleanIsolated,
|
||||
operators.Print3DCleanDistorted,
|
||||
operators.Print3DCleanThin,
|
||||
operators.Print3DCleanNonManifold,
|
||||
|
||||
operators.Print3DSelectReport,
|
||||
|
||||
|
|
|
@ -433,6 +433,160 @@ class Print3DCleanDistorted(Operator):
|
|||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class Print3DCleanNonManifold(Operator):
|
||||
"""Cleanup problems, like holes, non-manifold vertices, and inverted normals"""
|
||||
bl_idname = "mesh.print3d_clean_non_manifold"
|
||||
bl_label = "Print3D Clean Non-Manifold and Inverted"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
threshold = bpy.props.FloatProperty(
|
||||
name="threshold",
|
||||
description="Minimum distance between elements to merge",
|
||||
default=0.0001,
|
||||
)
|
||||
sides = bpy.props.IntProperty(
|
||||
name="sides",
|
||||
description="Number of sides in hole required to fill",
|
||||
default=4,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
self.context = context
|
||||
mode_orig = context.mode
|
||||
|
||||
self.setup_environment()
|
||||
bm_key_orig = self.elem_count(context)
|
||||
|
||||
self.delete_loose()
|
||||
self.remove_doubles(self.threshold)
|
||||
self.dissolve_degenerate(self.threshold)
|
||||
|
||||
# may take a while
|
||||
self.fix_non_manifold(context, self.sides)
|
||||
|
||||
self.make_normals_consistently_outwards()
|
||||
|
||||
bm_key = self.elem_count(context)
|
||||
|
||||
if mode_orig != 'EDIT_MESH':
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
self.report(
|
||||
{'INFO'},
|
||||
"Modified Verts:%+d, Edges:%+d, Faces:%+d" %
|
||||
(bm_key[0] - bm_key_orig[0],
|
||||
bm_key[1] - bm_key_orig[1],
|
||||
bm_key[2] - bm_key_orig[2],
|
||||
))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def elem_count(context):
|
||||
bm = bmesh.from_edit_mesh(context.edit_object.data)
|
||||
return len(bm.verts), len(bm.edges), len(bm.faces)
|
||||
|
||||
@staticmethod
|
||||
def setup_environment():
|
||||
"""set the mode as edit, select mode as vertices, and reveal hidden vertices"""
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_mode(type='VERT')
|
||||
bpy.ops.mesh.reveal()
|
||||
|
||||
@staticmethod
|
||||
def remove_doubles(threshold):
|
||||
"""remove duplicate vertices"""
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.remove_doubles(threshold=threshold)
|
||||
|
||||
@staticmethod
|
||||
def delete_loose():
|
||||
"""delete loose vertices/edges/faces"""
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.delete_loose()
|
||||
|
||||
@staticmethod
|
||||
def dissolve_degenerate(threshold):
|
||||
"""dissolve zero area faces and zero length edges"""
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.dissolve_degenerate(threshold=threshold)
|
||||
|
||||
@staticmethod
|
||||
def make_normals_consistently_outwards():
|
||||
"""have all normals face outwards"""
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.normals_make_consistent()
|
||||
|
||||
@classmethod
|
||||
def fix_non_manifold(cls, context, sides):
|
||||
"""naive iterate-until-no-more approach for fixing manifolds"""
|
||||
total_non_manifold = cls.count_non_manifold_verts(context)
|
||||
|
||||
if not total_non_manifold:
|
||||
return
|
||||
|
||||
bm_states = set()
|
||||
bm_key = cls.elem_count(context)
|
||||
bm_states.add(bm_key)
|
||||
|
||||
while True:
|
||||
cls.fill_non_manifold(sides)
|
||||
|
||||
cls.delete_newly_generated_non_manifold_verts()
|
||||
|
||||
bm_key = cls.elem_count(context)
|
||||
if bm_key in bm_states:
|
||||
break
|
||||
else:
|
||||
bm_states.add(bm_key)
|
||||
|
||||
@staticmethod
|
||||
def select_non_manifold_verts(
|
||||
use_wire=False,
|
||||
use_boundary=False,
|
||||
use_multi_face=False,
|
||||
use_non_contiguous=False,
|
||||
use_verts=False,
|
||||
):
|
||||
"""select non-manifold vertices"""
|
||||
bpy.ops.mesh.select_non_manifold(
|
||||
extend=False,
|
||||
use_wire=use_wire,
|
||||
use_boundary=use_boundary,
|
||||
use_multi_face=use_multi_face,
|
||||
use_non_contiguous=use_non_contiguous,
|
||||
use_verts=use_verts,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def count_non_manifold_verts(cls, context):
|
||||
"""return a set of coordinates of non-manifold vertices"""
|
||||
cls.select_non_manifold_verts(
|
||||
use_wire=True,
|
||||
use_boundary=True,
|
||||
use_verts=True,
|
||||
)
|
||||
|
||||
bm = bmesh.from_edit_mesh(context.edit_object.data)
|
||||
return sum((1 for v in bm.verts if v.select))
|
||||
|
||||
@classmethod
|
||||
def fill_non_manifold(cls, sides):
|
||||
"""fill holes and then fill in any remnant non-manifolds"""
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.fill_holes(sides=sides)
|
||||
|
||||
# fill selected edge faces, which could be additional holes
|
||||
cls.select_non_manifold_verts(use_boundary=True)
|
||||
bpy.ops.mesh.fill()
|
||||
|
||||
@classmethod
|
||||
def delete_newly_generated_non_manifold_verts(cls):
|
||||
"""delete any newly generated vertices from the filling repair"""
|
||||
cls.select_non_manifold_verts(use_wire=True, use_verts=True)
|
||||
bpy.ops.mesh.delete(type='VERT')
|
||||
|
||||
|
||||
class Print3DCleanThin(Operator):
|
||||
"""Ensure minimum thickness"""
|
||||
bl_idname = "mesh.print3d_clean_thin"
|
||||
|
|
|
@ -105,6 +105,8 @@ class Print3DToolBar:
|
|||
rowsub = col.row(align=True)
|
||||
rowsub.operator("mesh.print3d_clean_distorted", text="Distorted")
|
||||
rowsub.prop(print_3d, "angle_distort", text="")
|
||||
col = layout.column()
|
||||
col.operator("mesh.print3d_clean_non_manifold", text="Non-Manifold")
|
||||
# XXX TODO
|
||||
# col.operator("mesh.print3d_clean_thin", text="Wall Thickness")
|
||||
|
||||
|
|
Loading…
Reference in New Issue