1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "author" : "Stanislav Blinov",
9 "blender" : (2, 80, 0),
10 "description" : "Cut Faces and Deselect Boundary operators",
17 def bmesh_from_object(object):
19 if object.mode
== 'EDIT':
20 bm
= bmesh
.from_edit_mesh(mesh
)
26 def bmesh_release(bm
, object):
28 bm
.select_flush_mode()
29 if object.mode
== 'EDIT':
30 bmesh
.update_edit_mesh(mesh
, loop_triangles
=True)
35 def calc_face(face
, keep_caps
=True):
39 def radial_loops(loop
):
40 next
= loop
.link_loop_radial_next
42 result
, next
= next
, next
.link_loop_radial_next
50 for loop
in face
.loops
:
52 # Iterate over selected adjacent faces
53 for radial_loop
in filter(lambda l
: l
.face
.select
, radial_loops(loop
)):
54 # Tag the edge if no other face done so already
59 adjacent_face
= radial_loop
.face
60 # Only walk adjacent face if current face tagged the edge
61 if adjacent_face
.tag
and self_selected
:
62 result
+= calc_face(adjacent_face
, keep_caps
)
65 (selected
, to_select
)[self_selected
].append(loop
)
67 for loop
in to_select
:
68 result
.append(loop
.edge
)
71 # Select opposite edge in quads
72 if keep_caps
and len(selected
) == 1 and len(face
.verts
) == 4:
73 result
.append(selected
[0].link_loop_next
.link_loop_next
.edge
)
77 def get_edge_rings(bm
, keep_caps
=True):
82 for edge
in face
.edges
: edge
.tag
= False
85 # fetch selected faces while setting up tags
86 selected_faces
= [ f
for f
in bm
.faces
if tag_face(f
) ]
91 # generate a list of edges to select:
92 # traversing only tagged faces, since calc_face can walk and untag islands
93 for face
in filter(lambda f
: f
.tag
, selected_faces
): edges
+= calc_face(face
, keep_caps
)
95 # housekeeping: clear tags
96 for face
in selected_faces
:
98 for edge
in face
.edges
: edge
.tag
= False
102 class MESH_xOT_deselect_boundary(bpy
.types
.Operator
):
103 """Deselect boundary edges of selected faces"""
104 bl_idname
= "mesh.ext_deselect_boundary"
105 bl_label
= "Deselect Boundary"
106 bl_options
= {'REGISTER', 'UNDO'}
108 keep_cap_edges
: bpy
.props
.BoolProperty(
109 name
= "Keep Cap Edges",
110 description
= "Keep quad strip cap edges selected",
114 def poll(cls
, context
):
115 active_object
= context
.active_object
116 return active_object
and active_object
.type == 'MESH' and active_object
.mode
== 'EDIT'
118 def execute(self
, context
):
119 object = context
.active_object
120 bm
= bmesh_from_object(object)
123 edges
= get_edge_rings(bm
, keep_caps
= self
.keep_cap_edges
)
125 self
.report({'WARNING'}, "No suitable selection found")
128 bpy
.ops
.mesh
.select_all(action
='DESELECT')
129 bm
.select_mode
= {'EDGE'}
133 context
.tool_settings
.mesh_select_mode
[:] = False, True, False
136 bmesh_release(bm
, object)
140 class MESH_xOT_cut_faces(bpy
.types
.Operator
):
141 """Cut selected faces, connecting through their adjacent edges"""
142 bl_idname
= "mesh.ext_cut_faces"
143 bl_label
= "Cut Faces"
144 bl_options
= {'REGISTER', 'UNDO'}
146 # from bmesh_operators.h
152 num_cuts
: bpy
.props
.IntProperty(
153 name
= "Number of Cuts",
157 subtype
= 'UNSIGNED')
159 use_single_edge
: bpy
.props
.BoolProperty(
160 name
= "Quad/Tri Mode",
161 description
= "Cut boundary faces",
164 corner_type
: bpy
.props
.EnumProperty(
165 items
= [('INNER_VERT', "Inner Vert", ""),
166 ('PATH', "Path", ""),
168 ('STRAIGHT_CUT', "Straight Cut", ""),],
169 name
= "Quad Corner Type",
170 description
= "How to subdivide quad corners",
171 default
= 'STRAIGHT_CUT')
173 use_grid_fill
: bpy
.props
.BoolProperty(
174 name
= "Use Grid Fill",
175 description
= "Fill fully enclosed faces with a grid",
179 def poll(cls
, context
):
180 active_object
= context
.active_object
181 return active_object
and active_object
.type == 'MESH' and active_object
.mode
== 'EDIT'
183 def cut_edges(self
, context
):
184 object = context
.active_object
185 bm
= bmesh_from_object(object)
188 edges
= get_edge_rings(bm
, keep_caps
= True)
190 self
.report({'WARNING'}, "No suitable selection found")
193 result
= bmesh
.ops
.subdivide_edges(
196 cuts
= int(self
.num_cuts
),
197 use_grid_fill
= bool(self
.use_grid_fill
),
198 use_single_edge
= bool(self
.use_single_edge
),
199 quad_corner_type
= str(self
.corner_type
))
201 bpy
.ops
.mesh
.select_all(action
='DESELECT')
202 bm
.select_mode
= {'EDGE'}
204 inner
= result
['geom_inner']
205 for edge
in filter(lambda e
: isinstance(e
, bmesh
.types
.BMEdge
), inner
):
209 bmesh_release(bm
, object)
213 def execute(self
, context
):
215 if not self
.cut_edges(context
):
218 context
.tool_settings
.mesh_select_mode
[:] = False, True, False
219 # Try to select all possible loops
220 bpy
.ops
.mesh
.loop_multi_select(ring
=False)
223 def menu_deselect_boundary(self
, context
):
224 self
.layout
.operator(MESH_xOT_deselect_boundary
.bl_idname
)
226 def menu_cut_faces(self
, context
):
227 self
.layout
.operator(MESH_xOT_cut_faces
.bl_idname
)
230 bpy
.utils
.register_class(MESH_xOT_deselect_boundary
)
231 bpy
.utils
.register_class(MESH_xOT_cut_faces
)
233 if __name__
!= "__main__":
234 bpy
.types
.VIEW3D_MT_select_edit_mesh
.append(menu_deselect_boundary
)
235 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.append(menu_cut_faces
)
238 bpy
.utils
.unregister_class(MESH_xOT_deselect_boundary
)
239 bpy
.utils
.unregister_class(MESH_xOT_cut_faces
)
241 if __name__
!= "__main__":
242 bpy
.types
.VIEW3D_MT_select_edit_mesh
.remove(menu_deselect_boundary
)
243 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.remove(menu_cut_faces
)
245 if __name__
== "__main__":