# SPDX-License-Identifier: GPL-2.0-or-later bl_info = { "name": "PKHG faces", "author": "PKHG", "version": (0, 0, 6), "blender": (2, 71, 0), "location": "View3D > Tools > PKHG (tab)", "description": "Faces selected will become added faces of different style", "warning": "", "doc_url": "", "category": "Mesh", } import bpy import bmesh from bpy.types import Operator from mathutils import Vector from bpy.props import ( BoolProperty, StringProperty, IntProperty, FloatProperty, EnumProperty, ) class MESH_OT_add_faces_to_object(Operator): bl_idname = "mesh.add_faces_to_object" bl_label = "Face Shape" bl_description = "Set parameters and build object with added faces" bl_options = {'REGISTER', 'UNDO', 'PRESET'} reverse_faces: BoolProperty( name="Reverse Faces", default=False, description="Revert the normals of selected faces" ) name_source_object: StringProperty( name="Mesh", description="Choose a Source Mesh", default="Cube" ) remove_start_faces: BoolProperty( name="Remove Start Faces", default=True, description="Make a choice about removal of Original Faces" ) base_height: FloatProperty( name="Base Height", min=-20, soft_max=10, max=20, default=0.2, description="Set general Base Height" ) use_relative_base_height: BoolProperty( name="Relative Base Height", default=False, description="Relative or absolute Base Height" ) second_height: FloatProperty( name="2nd height", min=-5, soft_max=5, max=20, default=0.2, description="Second height for various shapes" ) width: FloatProperty( name="Width Faces", min=-20, max=20, default=0.5, description="Set general width" ) repeat_extrude: IntProperty( name="Repeat", min=1, soft_max=5, max=20, description="For longer base" ) move_inside: FloatProperty( name="Move Inside", min=0.0, max=1.0, default=0.5, description="How much move to inside" ) thickness: FloatProperty( name="Thickness", soft_min=0.01, min=0, soft_max=5.0, max=20.0, default=0 ) depth: FloatProperty( name="Depth", min=-5, soft_max=5.0, max=20.0, default=0 ) collapse_edges: BoolProperty( name="Make Point", default=False, description="Collapse the vertices of edges" ) spike_base_width: FloatProperty( name="Spike Base Width", default=0.4, min=-4.0, soft_max=1, max=20, description="Base width of a spike" ) base_height_inset: FloatProperty( name="Base Height Inset", default=0.0, min=-5, max=5, description="To elevate or drop the Base height Inset" ) top_spike: FloatProperty( name="Top Spike", default=1.0, min=-10.0, max=10.0, description="The Base Height of a spike" ) top_extra_height: FloatProperty( name="Top Extra Height", default=0.0, min=-10.0, max=10.0, description="Add extra height" ) step_with_real_spike: BoolProperty( name="Step with Real Spike", default=False, description="In stepped, use a real spike" ) use_relative: BoolProperty( name="Use Relative", default=False, description="Change size using area, min or max" ) face_types: EnumProperty( name="Face Types", description="Different types of Faces", default="no", items=[ ('no', "Pick an Option", "Choose one of the available options"), ('open_inset', "Open Inset", "Inset without closing faces (holes)"), ('with_base', "With Base", "Base and ..."), ('clsd_vertical', "Closed Vertical", "Closed Vertical"), ('open_vertical', "Open Vertical", "Open Vertical"), ('spiked', "Spiked", "Spike"), ('stepped', "Stepped", "Stepped"), ('boxed', "Boxed", "Boxed"), ('bar', "Bar", "Bar"), ] ) strange_boxed_effect: BoolProperty( name="Strange Effect", default=False, description="Do not show one extrusion" ) use_boundary: BoolProperty( name="Use Boundary", default=True ) use_even_offset: BoolProperty( name="Even Offset", default=True ) use_relative_offset: BoolProperty( name="Relative Offset", default=True ) use_edge_rail: BoolProperty( name="Edge Rail", default=False ) use_outset: BoolProperty( name="Outset", default=False ) use_select_inset: BoolProperty( name="Inset", default=False ) use_interpolate: BoolProperty( name="Interpolate", default=True ) @classmethod def poll(cls, context): result = False active_object = context.active_object if active_object: mesh_objects_name = [el.name for el in bpy.data.objects if el.type == "MESH"] if active_object.name in mesh_objects_name: result = True return result def draw(self, context): layout = self.layout col = layout.column() col.separator() col.label(text="Using Active Object", icon="INFO") col.separator() col.label(text="Face Types:") col.prop(self, "face_types", text="") col.separator() col.prop(self, "use_relative") if self.face_types == "open_inset": col.prop(self, "move_inside") col.prop(self, "base_height") elif self.face_types == "with_base": col.prop(self, "move_inside") col.prop(self, "base_height") col.prop(self, "second_height") col.prop(self, "width") elif self.face_types == "clsd_vertical": col.prop(self, "base_height") elif self.face_types == "open_vertical": col.prop(self, "base_height") elif self.face_types == "boxed": col.prop(self, "move_inside") col.prop(self, "base_height") col.prop(self, "top_spike") col.prop(self, "strange_boxed_effect") elif self.face_types == "spiked": col.prop(self, "spike_base_width") col.prop(self, "base_height_inset") col.prop(self, "top_spike") elif self.face_types == "bar": col.prop(self, "spike_base_width") col.prop(self, "top_spike") col.prop(self, "top_extra_height") elif self.face_types == "stepped": col.prop(self, "spike_base_width") col.prop(self, "base_height_inset") col.prop(self, "top_extra_height") col.prop(self, "second_height") col.prop(self, "step_with_real_spike") def execute(self, context): obj_name = self.name_source_object face_type = self.face_types is_selected = check_is_selected() if not is_selected: self.report({'WARNING'}, "Operation Cancelled. No selected Faces found on the Active Object") return {'CANCELLED'} if face_type == "spiked": Spiked(spike_base_width=self.spike_base_width, base_height_inset=self.base_height_inset, top_spike=self.top_spike, top_relative=self.use_relative) elif face_type == "boxed": startinfo = prepare(self, context, self.remove_start_faces) bm = startinfo['bm'] top = self.top_spike obj = startinfo['obj'] obj_matrix_local = obj.matrix_local distance = None base_heights = None t = self.move_inside areas = startinfo['areas'] base_height = self.base_height if self.use_relative: distance = [min(t * area, 1.0) for i, area in enumerate(areas)] base_heights = [base_height * area for i, area in enumerate(areas)] else: distance = [t] * len(areas) base_heights = [base_height] * len(areas) rings = startinfo['rings'] centers = startinfo['centers'] normals = startinfo['normals'] for i in range(len(rings)): make_one_inset(self, context, bm=bm, ringvectors=rings[i], center=centers[i], normal=normals[i], t=distance[i], base_height=base_heights[i]) bpy.ops.mesh.select_mode(type="EDGE") bpy.ops.mesh.select_more() bpy.ops.mesh.select_more() bpy.ops.object.mode_set(mode='OBJECT') # PKHG>INFO base extrusion done and set to the mesh # PKHG>INFO if the extrusion is NOT done ... it'll look strange soon! if not self.strange_boxed_effect: bpy.ops.object.mode_set(mode='EDIT') obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) bmfaces = [face for face in bm.faces if face.select] res = extrude_faces(self, context, bm=bm, face_l=bmfaces) ring_edges = [face.edges[:] for face in res] bpy.ops.object.mode_set(mode='OBJECT') # PKHG>INFO now the extruded facec have to move in normal direction bpy.ops.object.mode_set(mode='EDIT') obj = bpy.context.view_layer.objects.active bm = bmesh.from_edit_mesh(obj.data) todo_faces = [face for face in bm.faces if face.select] for face in todo_faces: bmesh.ops.translate(bm, vec=face.normal * top, space=obj_matrix_local, verts=face.verts) bpy.ops.object.mode_set(mode='OBJECT') elif face_type == "stepped": Stepped(spike_base_width=self.spike_base_width, base_height_inset=self.base_height_inset, top_spike=self.second_height, top_extra_height=self.top_extra_height, use_relative_offset=self.use_relative, with_spike=self.step_with_real_spike) elif face_type == "open_inset": startinfo = prepare(self, context, self.remove_start_faces) bm = startinfo['bm'] # PKHG>INFO adjust for relative, via areas t = self.move_inside areas = startinfo['areas'] base_height = self.base_height base_heights = None distance = None if self.use_relative: distance = [min(t * area, 1.0) for i, area in enumerate(areas)] base_heights = [base_height * area for i, area in enumerate(areas)] else: distance = [t] * len(areas) base_heights = [base_height] * len(areas) rings = startinfo['rings'] centers = startinfo['centers'] normals = startinfo['normals'] for i in range(len(rings)): make_one_inset(self, context, bm=bm, ringvectors=rings[i], center=centers[i], normal=normals[i], t=distance[i], base_height=base_heights[i]) bpy.ops.object.mode_set(mode='OBJECT') elif face_type == "with_base": startinfo = prepare(self, context, self.remove_start_faces) bm = startinfo['bm'] obj = startinfo['obj'] object_matrix = obj.matrix_local # PKHG>INFO for relative (using areas) t = self.move_inside areas = startinfo['areas'] base_height = self.base_height distance = None base_heights = None if self.use_relative: distance = [min(t * area, 1.0) for i, area in enumerate(areas)] base_heights = [base_height * area for i, area in enumerate(areas)] else: distance = [t] * len(areas) base_heights = [base_height] * len(areas) next_rings = [] rings = startinfo['rings'] centers = startinfo['centers'] normals = startinfo['normals'] for i in range(len(rings)): next_rings.append(make_one_inset(self, context, bm=bm, ringvectors=rings[i], center=centers[i], normal=normals[i], t=distance[i], base_height=base_heights[i])) prepare_ring = extrude_edges(self, context, bm=bm, edge_l_l=next_rings) second_height = self.second_height width = self.width vectors = [[ele.verts[:] for ele in edge] for edge in prepare_ring] n_ring_vecs = [] for rings in vectors: v = [] for edgv in rings: v.extend(edgv) # PKHF>INFO no double verts allowed, coming from two adjacents edges! bm.verts.ensure_lookup_table() vv = list(set([ele.index for ele in v])) vvv = [bm.verts[i].co for i in vv] n_ring_vecs.append(vvv) for i, ring in enumerate(n_ring_vecs): make_one_inset(self, context, bm=bm, ringvectors=ring, center=centers[i], normal=normals[i], t=width, base_height=base_heights[i] + second_height) bpy.ops.object.mode_set(mode='OBJECT') else: if face_type == "clsd_vertical": obj_name = context.active_object.name ClosedVertical(name=obj_name, base_height=self.base_height, use_relative_base_height=self.use_relative) elif face_type == "open_vertical": obj_name = context.active_object.name OpenVertical(name=obj_name, base_height=self.base_height, use_relative_base_height=self.use_relative) elif face_type == "bar": startinfo = prepare(self, context, self.remove_start_faces) result = [] bm = startinfo['bm'] rings = startinfo['rings'] centers = startinfo['centers'] normals = startinfo['normals'] spike_base_width = self.spike_base_width for i, ring in enumerate(rings): result.append(make_one_inset(self, context, bm=bm, ringvectors=ring, center=centers[i], normal=normals[i], t=spike_base_width)) next_ring_edges_list = extrude_edges(self, context, bm=bm, edge_l_l=result) top_spike = self.top_spike fac = top_spike object_matrix = startinfo['obj'].matrix_local for i in range(len(next_ring_edges_list)): translate_ONE_ring( self, context, bm=bm, object_matrix=object_matrix, ring_edges=next_ring_edges_list[i], normal=normals[i], distance=fac ) next_ring_edges_list_2 = extrude_edges(self, context, bm=bm, edge_l_l=next_ring_edges_list) top_extra_height = self.top_extra_height for i in range(len(next_ring_edges_list_2)): move_corner_vecs_outside( self, context, bm=bm, edge_list=next_ring_edges_list_2[i], center=centers[i], normal=normals[i], base_height_erlier=fac + top_extra_height, distance=fac ) bpy.ops.mesh.select_mode(type="VERT") bpy.ops.mesh.select_more() bpy.ops.object.mode_set(mode='OBJECT') return {'FINISHED'} def find_one_ring(sel_vertices): ring0 = sel_vertices.pop(0) to_delete = [] for i, edge in enumerate(sel_vertices): len_nu = len(ring0) if len(ring0 - edge) < len_nu: to_delete.append(i) ring0 = ring0.union(edge) to_delete.reverse() for el in to_delete: sel_vertices.pop(el) return (ring0, sel_vertices) class Stepped: def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2, top_relative=False, top_extra_height=0, use_relative_offset=False, with_spike=False): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset, use_edge_rail=False, thickness=top_extra_height, depth=base_height_inset, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=use_relative_offset, use_edge_rail=False, thickness=spike_base_width, depth=0, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) if with_spike: bpy.ops.mesh.merge(type='COLLAPSE') bpy.ops.object.mode_set(mode='OBJECT') class Spiked: def __init__(self, spike_base_width=0.5, base_height_inset=0.0, top_spike=0.2, top_relative=False): obj = bpy.context.active_object bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=False, thickness=spike_base_width, depth=base_height_inset, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=top_relative, use_edge_rail=False, thickness=0, depth=top_spike, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bm = bmesh.from_edit_mesh(obj.data) bpy.ops.mesh.merge(type='COLLAPSE') bpy.ops.object.mode_set(mode='OBJECT') class ClosedVertical: def __init__(self, name="Plane", base_height=1, use_relative_base_height=False): obj = bpy.data.objects[name] bpy.ops.object.mode_set(mode='OBJECT') bm = bmesh.new() bm.from_mesh(obj.data) # PKHG>INFO deselect chosen faces sel = [f for f in bm.faces if f.select] for f in sel: f.select = False res = bmesh.ops.extrude_discrete_faces(bm, faces=sel) # PKHG>INFO select extruded faces for f in res['faces']: f.select = True factor = base_height for face in res['faces']: if use_relative_base_height: area = face.calc_area() factor = area * base_height else: factor = base_height for el in face.verts: tmp = el.co + face.normal * factor el.co = tmp me = bpy.data.meshes[name] bm.to_mesh(me) bm.free() class OpenVertical: def __init__(self, name="Plane", base_height=1, use_relative_base_height=False): obj = bpy.data.objects[name] bpy.ops.object.mode_set(mode='OBJECT') bm = bmesh.new() bm.from_mesh(obj.data) # PKHG>INFO deselect chosen faces sel = [f for f in bm.faces if f.select] for f in sel: f.select = False res = bmesh.ops.extrude_discrete_faces(bm, faces=sel) # PKHG>INFO select extruded faces for f in res['faces']: f.select = True # PKHG>INFO adjust extrusion by a vector factor = base_height for face in res['faces']: if use_relative_base_height: area = face.calc_area() factor = area * base_height else: factor = base_height for el in face.verts: tmp = el.co + face.normal * factor el.co = tmp me = bpy.data.meshes[name] bm.to_mesh(me) bm.free() bpy.ops.object.editmode_toggle() bpy.ops.mesh.delete(type='FACE') bpy.ops.object.editmode_toggle() class StripFaces: def __init__(self, use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=True, thickness=0.0, depth=0.0, use_outset=False, use_select_inset=False, use_individual=True, use_interpolate=True): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.inset( use_boundary=use_boundary, use_even_offset=True, use_relative_offset=False, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=use_outset, use_select_inset=use_select_inset, use_individual=use_individual, use_interpolate=use_interpolate ) bpy.ops.object.mode_set(mode='OBJECT') # PKHG>IMFO only 3 parameters inc execution context supported!! if False: bpy.ops.mesh.inset( use_boundary, use_even_offset, use_relative_offset, use_edge_rail, thickness, depth, use_outset, use_select_inset, use_individual, use_interpolate ) elif type == 0: bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, use_select_inset=False, use_individual=True, use_interpolate=True ) elif type == 1: bpy.ops.mesh.inset( use_boundary=True, use_even_offset=True, use_relative_offset=False, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, use_select_inset=False, use_individual=True, use_interpolate=False ) bpy.ops.mesh.delete(type='FACE') elif type == 2: bpy.ops.mesh.inset( use_boundary=True, use_even_offset=False, use_relative_offset=True, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=False, use_select_inset=False, use_individual=True, use_interpolate=False ) bpy.ops.mesh.delete(type='FACE') elif type == 3: bpy.ops.mesh.inset( use_boundary=True, use_even_offset=False, use_relative_offset=True, use_edge_rail=True, thickness=depth, depth=thickness, use_outset=False, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.delete(type='FACE') elif type == 4: bpy.ops.mesh.inset( use_boundary=True, use_even_offset=False, use_relative_offset=True, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.inset( use_boundary=True, use_even_offset=False, use_relative_offset=True, use_edge_rail=True, thickness=thickness, depth=depth, use_outset=True, use_select_inset=False, use_individual=True, use_interpolate=True ) bpy.ops.mesh.delete(type='FACE') bpy.ops.object.mode_set(mode='OBJECT') def check_is_selected(): is_selected = False for face in bpy.context.active_object.data.polygons: if face.select: is_selected = True break return is_selected def prepare(self, context, remove_start_faces=True): """ Start for a face selected change of faces select an object of type mesh, with activated several (all) faces """ obj = bpy.context.view_layer.objects.active bpy.ops.object.mode_set(mode='OBJECT') selectedpolygons = [el for el in obj.data.polygons if el.select] # PKHG>INFO copies of the vectors are needed, otherwise Blender crashes! centers = [face.center for face in selectedpolygons] centers_copy = [Vector((el[0], el[1], el[2])) for el in centers] normals = [face.normal for face in selectedpolygons] normals_copy = [Vector((el[0], el[1], el[2])) for el in normals] vertindicesofpolgons = [ [vert for vert in face.vertices] for face in selectedpolygons ] vertVectorsOfSelectedFaces = [ [obj.data.vertices[ind].co for ind in vertIndiceofface] for vertIndiceofface in vertindicesofpolgons ] vertVectorsOfSelectedFaces_copy = [ [Vector((el[0], el[1], el[2])) for el in listofvecs] for listofvecs in vertVectorsOfSelectedFaces ] bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(obj.data) selected_bm_faces = [ele for ele in bm.faces if ele.select] selected_edges_per_face_ind = [ [ele.index for ele in face.edges] for face in selected_bm_faces ] indices = [el.index for el in selectedpolygons] selected_faces_areas = [bm.faces[:][i] for i in indices] tmp_area = [el.calc_area() for el in selected_faces_areas] # PKHG>INFO, selected faces are removed, only their edges are used! if remove_start_faces: bpy.ops.mesh.delete(type='ONLY_FACE') bpy.ops.object.mode_set(mode='OBJECT') obj.data.update() bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(obj.data) bm.verts.ensure_lookup_table() bm.faces.ensure_lookup_table() start_ring_raw = [ [bm.verts[ind].index for ind in vertIndiceofface] for vertIndiceofface in vertindicesofpolgons ] start_ring = [] for el in start_ring_raw: start_ring.append(set(el)) bm.edges.ensure_lookup_table() bm_selected_edges_l_l = [ [bm.edges[i] for i in bm_ind_list] for bm_ind_list in selected_edges_per_face_ind ] result = { 'obj': obj, 'centers': centers_copy, 'normals': normals_copy, 'rings': vertVectorsOfSelectedFaces_copy, 'bm': bm, 'areas': tmp_area, 'startBMRingVerts': start_ring, 'base_edges': bm_selected_edges_l_l } return result def make_one_inset(self, context, bm=None, ringvectors=None, center=None, normal=None, t=None, base_height=0): # a face will get 'inserted' faces to create (normally) a hole if t is > 0 and < 1) tmp = [] for el in ringvectors: tmp.append((el * (1 - t) + center * t) + normal * base_height) tmp = [bm.verts.new(v) for v in tmp] # the new corner bmvectors # PKHG>INFO so to say sentinells, to use ONE for ... tmp.append(tmp[0]) vectorsFace_i = [bm.verts.new(v) for v in ringvectors] vectorsFace_i.append(vectorsFace_i[0]) myres = [] for ii in range(len(vectorsFace_i) - 1): # PKHG>INFO next line: sequence is important! for added edge bmvecs = [vectorsFace_i[ii], vectorsFace_i[ii + 1], tmp[ii + 1], tmp[ii]] res = bm.faces.new(bmvecs) myres.append(res.edges[2]) myres[-1].select = True # PKHG>INFO to be used later selected! return (myres) def extrude_faces(self, context, bm=None, face_l=None): # to make a ring extrusion res = bmesh.ops.extrude_discrete_faces(bm, faces=face_l)['faces'] for face in res: face.select = True return res def extrude_edges(self, context, bm=None, edge_l_l=None): # to make a ring extrusion all_results = [] for edge_l in edge_l_l: for edge in edge_l: edge.select = False res = bmesh.ops.extrude_edge_only(bm, edges=edge_l) tmp = [ele for ele in res['geom'] if isinstance(ele, bmesh.types.BMEdge)] for edge in tmp: edge.select = True all_results.append(tmp) return all_results def translate_ONE_ring(self, context, bm=None, object_matrix=None, ring_edges=None, normal=(0, 0, 1), distance=0.5): # translate a ring in given (normal?!) direction with given (global) amount tmp = [] for edge in ring_edges: tmp.extend(edge.verts[:]) # PKHG>INFO no double vertices allowed by bmesh! tmp = set(tmp) tmp = list(tmp) bmesh.ops.translate(bm, vec=normal * distance, space=object_matrix, verts=tmp) # PKHG>INFO relevant edges will stay selected return ring_edges def move_corner_vecs_outside(self, context, bm=None, edge_list=None, center=None, normal=None, base_height_erlier=0.5, distance=0.5): # move corners (outside meant mostly) dependent on the parameters tmp = [] for edge in edge_list: tmp.extend([ele for ele in edge.verts if isinstance(ele, bmesh.types.BMVert)]) # PKHG>INFO to remove vertices, they are all used twice in the ring! tmp = set(tmp) tmp = list(tmp) for i in range(len(tmp)): vec = tmp[i].co direction = vec + (vec - (normal * base_height_erlier + center)) * distance tmp[i].co = direction # define classes for registration classes = ( MESH_OT_add_faces_to_object, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) if __name__ == "__main__": register()