Welcome to mirror list, hosted at ThFree Co, Russian Federation.

mesh_cut_faces.py « mesh_tools - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: c3e999c386e06a64ca108e25a0c807dbfbe59bfd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 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",
    "category" : "Mesh",
}

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()