Skip to content
Snippets Groups Projects
mesh_extrude_and_reshape.py 12.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • # 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()