# SPDX-License-Identifier: GPL-2.0-or-later # Contact for more information about the Addon: # Email: germano.costa@ig.com.br # Twitter: wii_mano @mano_wii bl_info = { "name": "Extrude and Reshape", "author": "Germano Cavalcante", "version": (0, 8, 1), "blender": (2, 80, 0), "location": "View3D > UI > Tools > Mesh Tools > Add: > Extrude Menu (Alt + E)", "description": "Extrude face and merge edge intersections " "between the mesh and the new edges", "doc_url": "http://blenderartists.org/forum/" "showthread.php?376618-Addon-Push-Pull-Face", "category": "Mesh", } import bpy import bmesh from mathutils.geometry import intersect_line_line from bpy.types import Operator class BVHco(): i = 0 c1x = 0.0 c1y = 0.0 c1z = 0.0 c2x = 0.0 c2y = 0.0 c2z = 0.0 def edges_BVH_overlap(bm, edges, epsilon=0.0001): bco = set() for e in edges: bvh = BVHco() bvh.i = e.index b1 = e.verts[0] b2 = e.verts[1] co1 = b1.co.x co2 = b2.co.x if co1 <= co2: bvh.c1x = co1 - epsilon bvh.c2x = co2 + epsilon else: bvh.c1x = co2 - epsilon bvh.c2x = co1 + epsilon co1 = b1.co.y co2 = b2.co.y if co1 <= co2: bvh.c1y = co1 - epsilon bvh.c2y = co2 + epsilon else: bvh.c1y = co2 - epsilon bvh.c2y = co1 + epsilon co1 = b1.co.z co2 = b2.co.z if co1 <= co2: bvh.c1z = co1 - epsilon bvh.c2z = co2 + epsilon else: bvh.c1z = co2 - epsilon bvh.c2z = co1 + epsilon bco.add(bvh) del edges overlap = {} oget = overlap.get for e1 in bm.edges: by = bz = True a1 = e1.verts[0] a2 = e1.verts[1] c1x = a1.co.x c2x = a2.co.x if c1x > c2x: tm = c1x c1x = c2x c2x = tm for bvh in bco: if c1x <= bvh.c2x and c2x >= bvh.c1x: if by: by = False c1y = a1.co.y c2y = a2.co.y if c1y > c2y: tm = c1y c1y = c2y c2y = tm if c1y <= bvh.c2y and c2y >= bvh.c1y: if bz: bz = False c1z = a1.co.z c2z = a2.co.z if c1z > c2z: tm = c1z c1z = c2z c2z = tm if c1z <= bvh.c2z and c2z >= bvh.c1z: e2 = bm.edges[bvh.i] if e1 != e2: overlap[e1] = oget(e1, set()).union({e2}) return overlap def intersect_edges_edges(overlap, precision=4): epsilon = .1**precision fpre_min = -epsilon fpre_max = 1 + epsilon splits = {} sp_get = splits.get new_edges1 = set() new_edges2 = set() targetmap = {} for edg1 in overlap: # print("***", ed1.index, "***") for edg2 in overlap[edg1]: a1 = edg1.verts[0] a2 = edg1.verts[1] b1 = edg2.verts[0] b2 = edg2.verts[1] # test if are linked if a1 in {b1, b2} or a2 in {b1, b2}: # print('linked') continue aco1, aco2 = a1.co, a2.co bco1, bco2 = b1.co, b2.co tp = intersect_line_line(aco1, aco2, bco1, bco2) if tp: p1, p2 = tp if (p1 - p2).to_tuple(precision) == (0, 0, 0): v = aco2 - aco1 f = p1 - aco1 x, y, z = abs(v.x), abs(v.y), abs(v.z) max1 = 0 if x >= y and x >= z else\ 1 if y >= x and y >= z else 2 fac1 = f[max1] / v[max1] v = bco2 - bco1 f = p2 - bco1 x, y, z = abs(v.x), abs(v.y), abs(v.z) max2 = 0 if x >= y and x >= z else\ 1 if y >= x and y >= z else 2 fac2 = f[max2] / v[max2] if fpre_min <= fac1 <= fpre_max: # print(edg1.index, 'can intersect', edg2.index) ed1 = edg1 elif edg1 in splits: for ed1 in splits[edg1]: a1 = ed1.verts[0] a2 = ed1.verts[1] vco1 = a1.co vco2 = a2.co v = vco2 - vco1 f = p1 - vco1 fac1 = f[max1] / v[max1] if fpre_min <= fac1 <= fpre_max: # print(e.index, 'can intersect', edg2.index) break else: # print(edg1.index, 'really does not intersect', edg2.index) continue else: # print(edg1.index, 'not intersect', edg2.index) continue if fpre_min <= fac2 <= fpre_max: # print(ed1.index, 'actually intersect', edg2.index) ed2 = edg2 elif edg2 in splits: for ed2 in splits[edg2]: b1 = ed2.verts[0] b2 = ed2.verts[1] vco1 = b1.co vco2 = b2.co v = vco2 - vco1 f = p2 - vco1 fac2 = f[max2] / v[max2] if fpre_min <= fac2 <= fpre_max: # print(ed1.index, 'actually intersect', e.index) break else: # print(ed1.index, 'really does not intersect', ed2.index) continue else: # print(ed1.index, 'not intersect', edg2.index) continue new_edges1.add(ed1) new_edges2.add(ed2) if abs(fac1) <= epsilon: nv1 = a1 elif fac1 + epsilon >= 1: nv1 = a2 else: ne1, nv1 = bmesh.utils.edge_split(ed1, a1, fac1) new_edges1.add(ne1) splits[edg1] = sp_get(edg1, set()).union({ne1}) if abs(fac2) <= epsilon: nv2 = b1 elif fac2 + epsilon >= 1: nv2 = b2 else: ne2, nv2 = bmesh.utils.edge_split(ed2, b1, fac2) new_edges2.add(ne2) splits[edg2] = sp_get(edg2, set()).union({ne2}) if nv1 != nv2: # necessary? targetmap[nv1] = nv2 return new_edges1, new_edges2, targetmap class ER_OT_Extrude_and_Reshape(Operator): bl_idname = "mesh.extrude_reshape" bl_label = "Extrude and Reshape" bl_description = "Push and pull face entities to sculpt 3d models" bl_options = {'REGISTER', 'GRAB_CURSOR', 'BLOCKING'} @classmethod def poll(cls, context): if context.mode=='EDIT_MESH': return True def modal(self, context, event): if self.confirm: sface = self.bm.faces.active if not sface: for face in self.bm.faces: if face.select is True: sface = face break else: return {'FINISHED'} # edges to intersect edges = set() [[edges.add(ed) for ed in v.link_edges] for v in sface.verts] overlap = edges_BVH_overlap(self.bm, edges, epsilon=0.0001) overlap = {k: v for k, v in overlap.items() if k not in edges} # remove repetition """ print([e.index for e in edges]) for a, b in overlap.items(): print(a.index, [e.index for e in b]) """ new_edges1, new_edges2, targetmap = intersect_edges_edges(overlap) pos_weld = set() for e in new_edges1: v1, v2 = e.verts if v1 in targetmap and v2 in targetmap: pos_weld.add((targetmap[v1], targetmap[v2])) if targetmap: bmesh.ops.weld_verts(self.bm, targetmap=targetmap) """ print([e.is_valid for e in new_edges1]) print([e.is_valid for e in new_edges2]) sp_faces1 = set() """ for e in pos_weld: v1, v2 = e lf1 = set(v1.link_faces) lf2 = set(v2.link_faces) rlfe = lf1.intersection(lf2) for f in rlfe: try: nf = bmesh.utils.face_split(f, v1, v2) # sp_faces1.update({f, nf[0]}) except: pass # sp_faces2 = set() for e in new_edges2: lfe = set(e.link_faces) v1, v2 = e.verts lf1 = set(v1.link_faces) lf2 = set(v2.link_faces) rlfe = lf1.intersection(lf2) for f in rlfe.difference(lfe): nf = bmesh.utils.face_split(f, v1, v2) # sp_faces2.update({f, nf[0]}) bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True) return {'FINISHED'} if self.cancel: return {'FINISHED'} self.cancel = event.type in {'ESC', 'NDOF_BUTTON_ESC'} self.confirm = event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'} return {'PASS_THROUGH'} def execute(self, context): self.mesh = context.object.data self.bm = bmesh.from_edit_mesh(self.mesh) try: selection = self.bm.select_history[-1] except: for face in self.bm.faces: if face.select is True: selection = face break else: return {'FINISHED'} if not isinstance(selection, bmesh.types.BMFace): bpy.ops.mesh.extrude_region_move('INVOKE_DEFAULT') return {'FINISHED'} else: face = selection # face.select = False bpy.ops.mesh.select_all(action='DESELECT') geom = [] for edge in face.edges: if abs(edge.calc_face_angle(0) - 1.5707963267948966) < 0.01: # self.angle_tolerance: geom.append(edge) ret_dict = bmesh.ops.extrude_discrete_faces(self.bm, faces=[face]) for face in ret_dict['faces']: self.bm.faces.active = face face.select = True sface = face dfaces = bmesh.ops.dissolve_edges( self.bm, edges=geom, use_verts=True, use_face_split=False ) bmesh.update_edit_mesh(self.mesh, loop_triangles=True, destructive=True) bpy.ops.transform.translate( 'INVOKE_DEFAULT', constraint_axis=(False, False, True), orient_type='NORMAL', release_confirm=True ) context.window_manager.modal_handler_add(self) self.cancel = False self.confirm = False return {'RUNNING_MODAL'} def operator_draw(self, context): layout = self.layout col = layout.column(align=True) col.operator("mesh.extrude_reshape") def register(): bpy.utils.register_class(ER_OT_Extrude_and_Reshape) def unregister(): bpy.utils.unregister_class(ER_OT_Extrude_and_Reshape) if __name__ == "__main__": register()