Newer
Older
# 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)
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
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()