Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / mesh_extra_tools / mesh_cut_faces.py
blob1522b159705eee1d86dbeb08dbcc0621055b85d9
1 # gpl author: Stanislav Blinov
3 bl_info = {
4 "name": "Cut Faces",
5 "author": "Stanislav Blinov",
6 "version": (1, 0, 0),
7 "blender": (2, 72, 0),
8 "description": "Cut Faces and Deselect Boundary operators",
9 "category": "Mesh", }
11 import bpy
12 import bmesh
13 from bpy.types import Operator
14 from bpy.props import (
15 BoolProperty,
16 IntProperty,
17 EnumProperty,
21 def bmesh_from_object(object):
22 mesh = object.data
23 if object.mode == 'EDIT':
24 bm = bmesh.from_edit_mesh(mesh)
25 else:
26 bm = bmesh.new()
27 bm.from_mesh(mesh)
28 return bm
31 def bmesh_release(bm, object):
32 mesh = object.data
33 bm.select_flush_mode()
34 if object.mode == 'EDIT':
35 bmesh.update_edit_mesh(mesh, True)
36 else:
37 bm.to_mesh(mesh)
38 bm.free()
41 def calc_face(face, keep_caps=True):
43 assert face.tag
45 def radial_loops(loop):
46 next = loop.link_loop_radial_next
47 while next != loop:
48 result, next = next, next.link_loop_radial_next
49 yield result
51 result = []
53 face.tag = False
54 selected = []
55 to_select = []
56 for loop in face.loops:
57 self_selected = False
58 # Iterate over selected adjacent faces
59 for radial_loop in filter(lambda l: l.face.select, radial_loops(loop)):
60 # Tag the edge if no other face done so already
61 if not loop.edge.tag:
62 loop.edge.tag = True
63 self_selected = True
65 adjacent_face = radial_loop.face
66 # Only walk adjacent face if current face tagged the edge
67 if adjacent_face.tag and self_selected:
68 result += calc_face(adjacent_face, keep_caps)
70 if loop.edge.tag:
71 (selected, to_select)[self_selected].append(loop)
73 for loop in to_select:
74 result.append(loop.edge)
75 selected.append(loop)
77 # Select opposite edge in quads
78 if keep_caps and len(selected) == 1 and len(face.verts) == 4:
79 result.append(selected[0].link_loop_next.link_loop_next.edge)
81 return result
84 def get_edge_rings(bm, keep_caps=True):
86 def tag_face(face):
87 if face.select:
88 face.tag = True
89 for edge in face.edges:
90 edge.tag = False
91 return face.select
93 # fetch selected faces while setting up tags
94 selected_faces = [f for f in bm.faces if tag_face(f)]
96 edges = []
98 try:
99 # generate a list of edges to select:
100 # traversing only tagged faces, since calc_face can walk and untag islands
101 for face in filter(lambda f: f.tag, selected_faces):
102 edges += calc_face(face, keep_caps)
103 finally:
104 # housekeeping: clear tags
105 for face in selected_faces:
106 face.tag = False
107 for edge in face.edges:
108 edge.tag = False
110 return edges
113 class MESH_xOT_deselect_boundary(Operator):
114 bl_idname = "mesh.ext_deselect_boundary"
115 bl_label = "Deselect Boundary"
116 bl_description = ("Deselect boundary edges of selected faces\n"
117 "Note: if all Faces are selected there is no boundary,\n"
118 "so the tool will not have results")
119 bl_options = {'REGISTER', 'UNDO'}
121 keep_cap_edges: BoolProperty(
122 name="Keep Cap Edges",
123 description="Keep quad strip cap edges selected",
124 default=False
127 @classmethod
128 def poll(cls, context):
129 active_object = context.active_object
130 return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
132 def execute(self, context):
133 object = context.active_object
134 bm = bmesh_from_object(object)
136 try:
137 edges = get_edge_rings(bm, keep_caps=self.keep_cap_edges)
138 if not edges:
139 self.report({'WARNING'}, "No suitable Face selection found. Operation cancelled")
140 return {'CANCELLED'}
142 bpy.ops.mesh.select_all(action='DESELECT')
143 bm.select_mode = {'EDGE'}
145 for edge in edges:
146 edge.select = True
147 context.tool_settings.mesh_select_mode[:] = False, True, False
149 finally:
150 bmesh_release(bm, object)
152 return {'FINISHED'}
155 class MESH_xOT_cut_faces(Operator):
156 bl_idname = "mesh.ext_cut_faces"
157 bl_label = "Cut Faces"
158 bl_description = "Cut selected faces, connected through their adjacent edges"
159 bl_options = {'REGISTER', 'UNDO'}
161 # from bmesh_operators.h
162 SUBD_INNERVERT = 0
163 SUBD_PATH = 1
164 SUBD_FAN = 2
165 SUBD_STRAIGHT_CUT = 3
167 num_cuts: IntProperty(
168 name="Number of Cuts",
169 default=1,
170 min=1,
171 max=100,
172 subtype='UNSIGNED'
174 use_single_edge: BoolProperty(
175 name="Quad/Tri Mode",
176 description="Cut boundary faces",
177 default=False
179 corner_type: EnumProperty(
180 items=[('SUBD_INNERVERT', "Inner Vert", ""),
181 ('SUBD_PATH', "Path", ""),
182 ('SUBD_FAN', "Fan", ""),
183 ('SUBD_STRAIGHT_CUT', "Straight Cut", ""),
185 name="Quad Corner Type",
186 description="How to subdivide quad corners",
187 default='SUBD_STRAIGHT_CUT'
189 use_grid_fill: BoolProperty(
190 name="Use Grid Fill",
191 description="Fill fully enclosed faces with a grid",
192 default=True
195 @classmethod
196 def poll(cls, context):
197 active_object = context.active_object
198 return active_object and active_object.type == 'MESH' and active_object.mode == 'EDIT'
200 def draw(self, context):
201 layout = self.layout
203 layout.label(text="Number of Cuts:")
204 layout.prop(self, "num_cuts", text="")
206 layout.prop(self, "use_single_edge")
207 layout.prop(self, "use_grid_fill")
209 layout.label(text="Quad Corner Type:")
210 layout.prop(self, "corner_type", text="")
212 def cut_edges(self, context):
213 object = context.active_object
214 bm = bmesh_from_object(object)
216 try:
217 edges = get_edge_rings(bm, keep_caps=True)
218 if not edges:
219 self.report({'WARNING'},
220 "No suitable Face selection found. Operation cancelled")
221 return False
223 result = bmesh.ops.subdivide_edges(
225 edges=edges,
226 cuts=int(self.num_cuts),
227 use_grid_fill=bool(self.use_grid_fill),
228 use_single_edge=bool(self.use_single_edge),
229 quad_corner_type=eval("self." + self.corner_type)
231 bpy.ops.mesh.select_all(action='DESELECT')
232 bm.select_mode = {'EDGE'}
234 inner = result['geom_inner']
235 for edge in filter(lambda e: isinstance(e, bmesh.types.BMEdge), inner):
236 edge.select = True
238 finally:
239 bmesh_release(bm, object)
241 return True
243 def execute(self, context):
245 if not self.cut_edges(context):
246 return {'CANCELLED'}
248 context.tool_settings.mesh_select_mode[:] = False, True, False
249 # Try to select all possible loops
250 bpy.ops.mesh.loop_multi_select(ring=False)
252 return {'FINISHED'}
255 def register():
256 bpy.utils.register_class(MESH_xOT_deselect_boundary)
257 bpy.utils.register_class(MESH_xOT_cut_faces)
260 def unregister():
261 bpy.utils.unregister_class(MESH_xOT_deselect_boundary)
262 bpy.utils.unregister_class(MESH_xOT_cut_faces)
265 if __name__ == "__main__":
266 register()