245 lines
7.3 KiB
Python
245 lines
7.3 KiB
Python
# 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()
|