Skip to content
Snippets Groups Projects
mesh_cut_faces.py 7.28 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    bl_info = {
        "name" : "Cut Faces",
        "author" : "Stanislav Blinov",
        "version" : (1, 0, 0),
        "blender" : (2, 80, 0),
        "description" : "Cut Faces and Deselect Boundary operators",
    
    
    import bpy
    import bmesh
    
    def bmesh_from_object(object):
        mesh = object.data
        if object.mode == 'EDIT':
            bm = bmesh.from_edit_mesh(mesh)
        else:
            bm = bmesh.new()
            bm.from_mesh(mesh)
        return bm
    
    def bmesh_release(bm, object):
        mesh = object.data
        bm.select_flush_mode()
        if object.mode == 'EDIT':
    
            bmesh.update_edit_mesh(mesh, loop_triangles=True)
    
        else:
            bm.to_mesh(mesh)
            bm.free()
    
    def calc_face(face, keep_caps=True):
    
        assert face.tag
    
        def radial_loops(loop):
            next = loop.link_loop_radial_next
            while next != loop:
                result, next = next, next.link_loop_radial_next
                yield result
    
        result = []
    
        face.tag = False
        selected = []
        to_select = []
        for loop in face.loops:
            self_selected = False
            # Iterate over selected adjacent faces
            for radial_loop in filter(lambda l: l.face.select, radial_loops(loop)):
                # Tag the edge if no other face done so already
                if not loop.edge.tag:
                    loop.edge.tag = True
                    self_selected = True
    
                adjacent_face = radial_loop.face
                # Only walk adjacent face if current face tagged the edge
                if adjacent_face.tag and self_selected:
                    result += calc_face(adjacent_face, keep_caps)
    
            if loop.edge.tag:
                (selected, to_select)[self_selected].append(loop)
    
        for loop in to_select:
            result.append(loop.edge)
            selected.append(loop)
    
        # Select opposite edge in quads
        if keep_caps and len(selected) == 1 and len(face.verts) == 4:
            result.append(selected[0].link_loop_next.link_loop_next.edge)
    
        return result
    
    def get_edge_rings(bm, keep_caps=True):
    
        def tag_face(face):
            if face.select:
                face.tag = True
                for edge in face.edges: edge.tag = False
            return face.select
    
        # fetch selected faces while setting up tags
        selected_faces = [ f for f in bm.faces if tag_face(f) ]
    
        edges = []
    
        try:
            # generate a list of edges to select:
            # traversing only tagged faces, since calc_face can walk and untag islands
            for face in filter(lambda f: f.tag, selected_faces): edges += calc_face(face, keep_caps)
        finally:
            # housekeeping: clear tags
            for face in selected_faces:
                face.tag = False
                for edge in face.edges: edge.tag = False
    
        return edges
    
    class MESH_xOT_deselect_boundary(bpy.types.Operator):
        """Deselect boundary edges of selected faces"""
        bl_idname = "mesh.ext_deselect_boundary"
        bl_label = "Deselect Boundary"
        bl_options = {'REGISTER', 'UNDO'}
    
        keep_cap_edges: bpy.props.BoolProperty(
            name        = "Keep Cap Edges",
            description = "Keep quad strip cap edges selected",
            default     = False)
    
        @classmethod
        def poll(cls, context):
            active_object = context.active_object
            return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
    
        def execute(self, context):
            object = context.active_object
            bm = bmesh_from_object(object)
    
            try:
                edges = get_edge_rings(bm, keep_caps = self.keep_cap_edges)
                if not edges:
                    self.report({'WARNING'}, "No suitable selection found")
                    return {'CANCELLED'}
    
                bpy.ops.mesh.select_all(action='DESELECT')
                bm.select_mode = {'EDGE'}
    
                for edge in edges:
                    edge.select = True
                context.tool_settings.mesh_select_mode[:] = False, True, False
    
            finally:
                bmesh_release(bm, object)
    
            return {'FINISHED'}
    
    class MESH_xOT_cut_faces(bpy.types.Operator):
        """Cut selected faces, connecting through their adjacent edges"""
        bl_idname = "mesh.ext_cut_faces"
        bl_label = "Cut Faces"
        bl_options = {'REGISTER', 'UNDO'}
    
        # from bmesh_operators.h
        INNERVERT    = 0
        PATH         = 1
        FAN          = 2
        STRAIGHT_CUT = 3
    
        num_cuts: bpy.props.IntProperty(
            name    = "Number of Cuts",
            default = 1,
            min     = 1,
            max     = 100,
            subtype = 'UNSIGNED')
    
        use_single_edge: bpy.props.BoolProperty(
            name        = "Quad/Tri Mode",
            description = "Cut boundary faces",
            default     = False)
    
        corner_type: bpy.props.EnumProperty(
            items = [('INNER_VERT', "Inner Vert", ""),
                     ('PATH', "Path", ""),
                     ('FAN', "Fan", ""),
                     ('STRAIGHT_CUT', "Straight Cut", ""),],
            name = "Quad Corner Type",
            description = "How to subdivide quad corners",
            default = 'STRAIGHT_CUT')
    
        use_grid_fill: bpy.props.BoolProperty(
            name        = "Use Grid Fill",
            description = "Fill fully enclosed faces with a grid",
            default     = True)
    
        @classmethod
        def poll(cls, context):
            active_object = context.active_object
            return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
    
        def cut_edges(self, context):
            object = context.active_object
            bm = bmesh_from_object(object)
    
            try:
                edges = get_edge_rings(bm, keep_caps = True)
                if not edges:
                    self.report({'WARNING'}, "No suitable selection found")
                    return False
    
                result = bmesh.ops.subdivide_edges(
                    bm,
                    edges = edges,
                    cuts = int(self.num_cuts),
                    use_grid_fill = bool(self.use_grid_fill),
                    use_single_edge = bool(self.use_single_edge),
                    quad_corner_type = str(self.corner_type))
    
                bpy.ops.mesh.select_all(action='DESELECT')
                bm.select_mode = {'EDGE'}
    
                inner = result['geom_inner']
                for edge in filter(lambda e: isinstance(e, bmesh.types.BMEdge), inner):
                    edge.select = True
    
            finally:
                bmesh_release(bm, object)
    
            return True
    
        def execute(self, context):
    
            if not self.cut_edges(context):
                return {'CANCELLED'}
    
            context.tool_settings.mesh_select_mode[:] = False, True, False
            # Try to select all possible loops
            bpy.ops.mesh.loop_multi_select(ring=False)
            return {'FINISHED'}
    
    def menu_deselect_boundary(self, context):
        self.layout.operator(MESH_xOT_deselect_boundary.bl_idname)
    
    def menu_cut_faces(self, context):
        self.layout.operator(MESH_xOT_cut_faces.bl_idname)
    
    def register():
        bpy.utils.register_class(MESH_xOT_deselect_boundary)
        bpy.utils.register_class(MESH_xOT_cut_faces)
    
        if __name__ != "__main__":
            bpy.types.VIEW3D_MT_select_edit_mesh.append(menu_deselect_boundary)
            bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu_cut_faces)
    
    def unregister():
        bpy.utils.unregister_class(MESH_xOT_deselect_boundary)
        bpy.utils.unregister_class(MESH_xOT_cut_faces)
    
        if __name__ != "__main__":
            bpy.types.VIEW3D_MT_select_edit_mesh.remove(menu_deselect_boundary)
            bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu_cut_faces)
    
    if __name__ == "__main__":
        register()