Import_3ds: Improved distance cue chunk import
[blender-addons.git] / mesh_tools / mesh_cut_faces.py
blobe9f21d19381db82878143e4e9e5ba6585abe776f
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name" : "Cut Faces",
7 "author" : "Stanislav Blinov",
8 "version" : (1, 0, 0),
9 "blender" : (2, 80, 0),
10 "description" : "Cut Faces and Deselect Boundary operators",
11 "category" : "Mesh",
14 import bpy
15 import bmesh
17 def bmesh_from_object(object):
18 mesh = object.data
19 if object.mode == 'EDIT':
20 bm = bmesh.from_edit_mesh(mesh)
21 else:
22 bm = bmesh.new()
23 bm.from_mesh(mesh)
24 return bm
26 def bmesh_release(bm, object):
27 mesh = object.data
28 bm.select_flush_mode()
29 if object.mode == 'EDIT':
30 bmesh.update_edit_mesh(mesh, loop_triangles=True)
31 else:
32 bm.to_mesh(mesh)
33 bm.free()
35 def calc_face(face, keep_caps=True):
37 assert face.tag
39 def radial_loops(loop):
40 next = loop.link_loop_radial_next
41 while next != loop:
42 result, next = next, next.link_loop_radial_next
43 yield result
45 result = []
47 face.tag = False
48 selected = []
49 to_select = []
50 for loop in face.loops:
51 self_selected = False
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
55 if not loop.edge.tag:
56 loop.edge.tag = True
57 self_selected = True
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)
64 if loop.edge.tag:
65 (selected, to_select)[self_selected].append(loop)
67 for loop in to_select:
68 result.append(loop.edge)
69 selected.append(loop)
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)
75 return result
77 def get_edge_rings(bm, keep_caps=True):
79 def tag_face(face):
80 if face.select:
81 face.tag = True
82 for edge in face.edges: edge.tag = False
83 return face.select
85 # fetch selected faces while setting up tags
86 selected_faces = [ f for f in bm.faces if tag_face(f) ]
88 edges = []
90 try:
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)
94 finally:
95 # housekeeping: clear tags
96 for face in selected_faces:
97 face.tag = False
98 for edge in face.edges: edge.tag = False
100 return edges
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",
111 default = False)
113 @classmethod
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)
122 try:
123 edges = get_edge_rings(bm, keep_caps = self.keep_cap_edges)
124 if not edges:
125 self.report({'WARNING'}, "No suitable selection found")
126 return {'CANCELLED'}
128 bpy.ops.mesh.select_all(action='DESELECT')
129 bm.select_mode = {'EDGE'}
131 for edge in edges:
132 edge.select = True
133 context.tool_settings.mesh_select_mode[:] = False, True, False
135 finally:
136 bmesh_release(bm, object)
138 return {'FINISHED'}
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
147 INNERVERT = 0
148 PATH = 1
149 FAN = 2
150 STRAIGHT_CUT = 3
152 num_cuts: bpy.props.IntProperty(
153 name = "Number of Cuts",
154 default = 1,
155 min = 1,
156 max = 100,
157 subtype = 'UNSIGNED')
159 use_single_edge: bpy.props.BoolProperty(
160 name = "Quad/Tri Mode",
161 description = "Cut boundary faces",
162 default = False)
164 corner_type: bpy.props.EnumProperty(
165 items = [('INNER_VERT', "Inner Vert", ""),
166 ('PATH', "Path", ""),
167 ('FAN', "Fan", ""),
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",
176 default = True)
178 @classmethod
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)
187 try:
188 edges = get_edge_rings(bm, keep_caps = True)
189 if not edges:
190 self.report({'WARNING'}, "No suitable selection found")
191 return False
193 result = bmesh.ops.subdivide_edges(
195 edges = 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):
206 edge.select = True
208 finally:
209 bmesh_release(bm, object)
211 return True
213 def execute(self, context):
215 if not self.cut_edges(context):
216 return {'CANCELLED'}
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)
221 return {'FINISHED'}
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)
229 def register():
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)
237 def unregister():
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__":
246 register()