Skip to content
Snippets Groups Projects
mesh_f2.py 21.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    # Updated for 2.8 jan 5 2019
    
        "name": "F2",
        "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
    
                  "(concept design), Adrian Rutkowski",
        "version": (1, 8, 4),
    
    Philipp Oeser's avatar
    Philipp Oeser committed
        "blender": (2, 80, 0),
    
        "location": "Editmode > F",
        "warning": "",
        "description": "Extends the 'Make Edge/Face' functionality",
    
        "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/f2.html",
    
    # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
    
    
    import bmesh
    import bpy
    import itertools
    import mathutils
    
    import math
    from mathutils import Vector
    
    # returns a custom data layer of the UV map, or None
    def get_uv_layer(ob, bm, mat_index):
        uv = None
        uv_layer = None
        if ob.material_slots:
            me = ob.data
            if me.uv_layers:
                uv = me.uv_layers.active.name
        # 'material_slots' is deprecated (Blender Internal)
        # else:
        #     mat = ob.material_slots[mat_index].material
        #     if mat is not None:
        #         slot = mat.texture_slots[mat.active_texture_index]
        #         if slot and slot.uv_layer:
        #             uv = slot.uv_layer
        #         else:
        #             for tex_slot in mat.texture_slots:
        #                 if tex_slot and tex_slot.uv_layer:
        #                     uv = tex_slot.uv_layer
        #                     break
        if uv:
            uv_layer = bm.loops.layers.uv.get(uv)
    
        return (uv_layer)
    
    
    
    # create a face from a single selected edge
    def quad_from_edge(bm, edge_sel, context, event):
    
        addon_prefs = context.preferences.addons[__name__].preferences
    
        ob = context.active_object
        region = context.region
        region_3d = context.space_data.region_3d
    
        # find linked edges that are open (<2 faces connected) and not part of
        # the face the selected edge belongs to
        all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
    
                      len(edge.link_faces) < 2 and edge != edge_sel and \
                      sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
                     for i in range(2)]
    
        if not all_edges[0] or not all_edges[1]:
            return
    
        # determine which edges to use, based on mouse cursor position
        mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
        optimal_edges = []
        for edges in all_edges:
            min_dist = False
            for edge in edges:
                vert = [vert for vert in edge.verts if not vert.select][0]
    
    Philipp Oeser's avatar
    Philipp Oeser committed
                world_pos = ob.matrix_world @ vert.co.copy()
    
                screen_pos = view3d_utils.location_3d_to_region_2d(region,
    
                dist = (mouse_pos - screen_pos).length
                if not min_dist or dist < min_dist[0]:
                    min_dist = (dist, edge, vert)
            optimal_edges.append(min_dist)
    
        # determine the vertices, which make up the quad
        v1 = edge_sel.verts[0]
        v2 = edge_sel.verts[1]
        edge_1 = optimal_edges[0][1]
        edge_2 = optimal_edges[1][1]
        v3 = optimal_edges[0][2]
        v4 = optimal_edges[1][2]
    
        # normal detection
        flip_align = True
        normal_edge = edge_1
        if not normal_edge.link_faces:
            normal_edge = edge_2
            if not normal_edge.link_faces:
                normal_edge = edge_sel
                if not normal_edge.link_faces:
                    # no connected faces, so no need to flip the face normal
                    flip_align = False
    
        if flip_align:  # there is a face to which the normal can be aligned
    
            ref_verts = [v for v in normal_edge.link_faces[0].verts]
    
            if v3 in ref_verts and v1 in ref_verts:
    
                va_1 = v3
                va_2 = v1
            elif normal_edge == edge_sel:
                va_1 = v1
                va_2 = v2
            else:
                va_1 = v2
                va_2 = v4
            if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
    
                    (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
    
                # reference verts are at start and end of the list -> shift list
                ref_verts = ref_verts[1:] + [ref_verts[0]]
            if ref_verts.index(va_1) > ref_verts.index(va_2):
                # connected face has same normal direction, so don't flip
                flip_align = False
    
        # material index detection
        ref_faces = edge_sel.link_faces
        if not ref_faces:
            ref_faces = edge_sel.verts[0].link_faces
        if not ref_faces:
            ref_faces = edge_sel.verts[1].link_faces
        if not ref_faces:
            mat_index = False
            smooth = False
        else:
            mat_index = ref_faces[0].material_index
            smooth = ref_faces[0].smooth
    
    
        if addon_prefs.quad_from_e_mat:
            mat_index = bpy.context.object.active_material_index
    
    
            if v3 == v4:
                # triangle (usually at end of quad-strip
                verts = [v3, v1, v2]
            else:
                # normal face creation
                verts = [v3, v1, v2, v4]
    
            if flip_align:
                verts.reverse()
            face = bm.faces.new(verts)
            if mat_index:
                face.material_index = mat_index
            face.smooth = smooth
        except:
            # face already exists
            return
    
        # change selection
        edge_sel.select = False
        for vert in edge_sel.verts:
            vert.select = False
        for edge in face.edges:
            if edge.index < 0:
                edge.select = True
        v3.select = True
        v4.select = True
    
    
        # adjust uv-map
        if __name__ != '__main__':
            if addon_prefs.adjustuv:
    
                uv_layer = get_uv_layer(ob, bm, mat_index)
                if uv_layer:
    
                    uv_ori = {}
                    for vert in [v1, v2, v3, v4]:
                        for loop in vert.link_loops:
                            if loop.face.index > -1:
                                uv_ori[loop.vert.index] = loop[uv_layer].uv
                    if len(uv_ori) == 4 or len(uv_ori) == 3:
                        for loop in face.loops:
    
                            if loop.vert.index in uv_ori:
                                loop[uv_layer].uv = uv_ori[loop.vert.index]
    
        # toggle mode, to force correct drawing
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
    
    
    # create a face from a single selected vertex, if it is an open vertex
    def quad_from_vertex(bm, vert_sel, context, event):
    
        addon_prefs = context.preferences.addons[__name__].preferences
    
        me = ob.data
    
        region = context.region
        region_3d = context.space_data.region_3d
    
        # find linked edges that are open (<2 faces connected)
        edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
        if len(edges) < 2:
            return
    
        # determine which edges to use, based on mouse cursor position
        min_dist = False
        mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
        for a, b in itertools.combinations(edges, 2):
            other_verts = [vert for edge in [a, b] for vert in edge.verts \
    
            mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
    
            new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
    
    Philipp Oeser's avatar
    Philipp Oeser committed
            world_pos = ob.matrix_world @ new_pos
    
            screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
    
            dist = (mouse_pos - screen_pos).length
            if not min_dist or dist < min_dist[0]:
                min_dist = (dist, (a, b), other_verts, new_pos)
    
        # create vertex at location mirrored in the line, connecting the open edges
        edges = min_dist[1]
        other_verts = min_dist[2]
        new_pos = min_dist[3]
        vert_new = bm.verts.new(new_pos)
    
        # normal detection
        flip_align = True
        normal_edge = edges[0]
        if not normal_edge.link_faces:
            normal_edge = edges[1]
            if not normal_edge.link_faces:
                # no connected faces, so no need to flip the face normal
    
                flip_align = False
        if flip_align:  # there is a face to which the normal can be aligned
    
            ref_verts = [v for v in normal_edge.link_faces[0].verts]
            if other_verts[0] in ref_verts:
                va_1 = other_verts[0]
                va_2 = vert_sel
            else:
                va_1 = vert_sel
                va_2 = other_verts[1]
            if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
    
                    (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
    
                # reference verts are at start and end of the list -> shift list
                ref_verts = ref_verts[1:] + [ref_verts[0]]
            if ref_verts.index(va_1) > ref_verts.index(va_2):
                # connected face has same normal direction, so don't flip
                flip_align = False
    
        # material index detection
        ref_faces = vert_sel.link_faces
        if not ref_faces:
            mat_index = False
            smooth = False
        else:
            mat_index = ref_faces[0].material_index
            smooth = ref_faces[0].smooth
    
    
        if addon_prefs.quad_from_v_mat:
            mat_index = bpy.context.object.active_material_index
    
    
        # create face between all 4 vertices involved
        verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
        if flip_align:
            verts.reverse()
        face = bm.faces.new(verts)
        if mat_index:
            face.material_index = mat_index
        face.smooth = smooth
    
        # change selection
        vert_new.select = True
        vert_sel.select = False
    
    
        # adjust uv-map
        if __name__ != '__main__':
            if addon_prefs.adjustuv:
    
                uv_layer = get_uv_layer(ob, bm, mat_index)
                if uv_layer:
    
                    uv_others = {}
                    uv_sel = None
                    uv_new = None
                    # get original uv coordinates
                    for i in range(2):
                        for loop in other_verts[i].link_loops:
                            if loop.face.index > -1:
                                uv_others[loop.vert.index] = loop[uv_layer].uv
                                break
                    if len(uv_others) == 2:
    
                        mid_other = (list(uv_others.values())[0] +
    
                                     list(uv_others.values())[1]) / 2
    
                        for loop in vert_sel.link_loops:
                            if loop.face.index > -1:
                                uv_sel = loop[uv_layer].uv
                                break
                        if uv_sel:
                            uv_new = 2 * (mid_other - uv_sel) + uv_sel
    
                    # set uv coordinates for new loops
                    if uv_new:
                        for loop in face.loops:
                            if loop.vert.index == -1:
                                x, y = uv_new
                            elif loop.vert.index in uv_others:
                                x, y = uv_others[loop.vert.index]
                            else:
                                x, y = uv_sel
                            loop[uv_layer].uv = (x, y)
    
    
        # toggle mode, to force correct drawing
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
    
    
    
    def expand_vert(self, context, event):
        addon_prefs = context.preferences.addons[__name__].preferences
        ob = context.active_object
        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        region = context.region
        region_3d = context.space_data.region_3d
        rv3d = context.space_data.region_3d
    
        for v in bm.verts:
            if v.select:
                v_active = v
    
        try:
            depth_location = v_active.co
        except:
            return {'CANCELLED'}
        # create vert in mouse cursor location
    
        mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
        location_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, depth_location)
    
        c_verts = []
        # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
        linked = v_active.link_edges
        for edges in linked:
            if len(edges.link_faces) < 2:
                edges.select = True
                for v in edges.verts:
                    if v is not v_active:
                        c_verts.append(v)
    
        # Compare distance in 2d between mouse and edges middle points
        screen_pos_va = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                              ob.matrix_world @ v_active.co)
        screen_pos_v1 = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                              ob.matrix_world @ c_verts[0].co)
        screen_pos_v2 = view3d_utils.location_3d_to_region_2d(region, region_3d,
                                                              ob.matrix_world @ c_verts[1].co)
    
        mid_pos_v1 = Vector(((screen_pos_va[0] + screen_pos_v1[0]) / 2, (screen_pos_va[1] + screen_pos_v1[1]) / 2))
        mid_pos_V2 = Vector(((screen_pos_va[0] + screen_pos_v2[0]) / 2, (screen_pos_va[1] + screen_pos_v2[1]) / 2))
    
        dist1 = math.log10(pow((mid_pos_v1[0] - mouse_pos[0]), 2) + pow((mid_pos_v1[1] - mouse_pos[1]), 2))
        dist2 = math.log10(pow((mid_pos_V2[0] - mouse_pos[0]), 2) + pow((mid_pos_V2[1] - mouse_pos[1]), 2))
    
        bm.normal_update()
        bm.verts.ensure_lookup_table()
    
        # Deselect not needed point and create new face
        if dist1 < dist2:
            c_verts[1].select = False
            lleft = c_verts[0].link_faces
    
        else:
            c_verts[0].select = False
            lleft = c_verts[1].link_faces
    
        lactive = v_active.link_faces
        # lverts = lactive[0].verts
    
        mat_index = lactive[0].material_index
        smooth = lactive[0].smooth
    
        for faces in lactive:
            if faces in lleft:
                cface = faces
                if len(faces.verts) == 3:
                    bm.normal_update()
                    bmesh.update_edit_mesh(obj.data)
                    bpy.ops.mesh.select_all(action='DESELECT')
                    v_active.select = True
                    bpy.ops.mesh.rip_edge_move('INVOKE_DEFAULT')
                    return {'FINISHED'}
    
        lverts = cface.verts
    
        # create triangle with correct normal orientation
        # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...
    
        # from L to R
        if dist1 < dist2:
            if (lverts[0] == v_active and lverts[3] == c_verts[0]) \
                    or (lverts[2] == v_active and lverts[1] == c_verts[0]) \
                    or (lverts[1] == v_active and lverts[0] == c_verts[0]) \
                    or (lverts[3] == v_active and lverts[2] == c_verts[0]):
                v_new = bm.verts.new(v_active.co)
                face_new = bm.faces.new((c_verts[0], v_new, v_active))
    
            elif (lverts[1] == v_active and lverts[2] == c_verts[0]) \
                    or (lverts[0] == v_active and lverts[1] == c_verts[0]) \
                    or (lverts[3] == v_active and lverts[0] == c_verts[0]) \
                    or (lverts[2] == v_active and lverts[3] == c_verts[0]):
                v_new = bm.verts.new(v_active.co)
                face_new = bm.faces.new((v_active, v_new, c_verts[0]))
    
            else:
                pass
        # from R to L
        else:
            if (lverts[2] == v_active and lverts[3] == c_verts[1]) \
                    or (lverts[0] == v_active and lverts[1] == c_verts[1]) \
                    or (lverts[1] == v_active and lverts[2] == c_verts[1]) \
                    or (lverts[3] == v_active and lverts[0] == c_verts[1]):
                v_new = bm.verts.new(v_active.co)
                face_new = bm.faces.new((v_active, v_new, c_verts[1]))
    
            elif (lverts[0] == v_active and lverts[3] == c_verts[1]) \
                    or (lverts[2] == v_active and lverts[1] == c_verts[1]) \
                    or (lverts[1] == v_active and lverts[0] == c_verts[1]) \
                    or (lverts[3] == v_active and lverts[2] == c_verts[1]):
                v_new = bm.verts.new(v_active.co)
                face_new = bm.faces.new((c_verts[1], v_new, v_active))
    
            else:
                pass
    
        # set smooth and mat based on starting face
        if addon_prefs.tris_from_v_mat:
            face_new.material_index = bpy.context.object.active_material_index
        else:
            face_new.material_index = mat_index
        face_new.smooth = smooth
    
        # update normals
        bpy.ops.mesh.select_all(action='DESELECT')
        v_new.select = True
        bm.select_history.add(v_new)
    
        bm.normal_update()
        bmesh.update_edit_mesh(obj.data)
        bpy.ops.transform.translate('INVOKE_DEFAULT')
    
    
    
    def checkforconnected(connection):
    
        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
    
        # Checks for number of edes or faces connected to selected vertex
        for v in bm.verts:
            if v.select:
                v_active = v
    
        if connection == 'faces':
    
        elif connection == 'edges':
    
            linked = v_active.link_edges
    
        bmesh.update_edit_mesh(obj.data)
        return len(linked)
    
    
    
    Bart Crouch's avatar
    Bart Crouch committed
    # autograb preference in addons panel
    class F2AddonPreferences(bpy.types.AddonPreferences):
        bl_idname = __name__
    
        adjustuv : bpy.props.BoolProperty(
            name="Adjust UV",
            description="Automatically update UV unwrapping",
            default=False)
        autograb : bpy.props.BoolProperty(
            name="Auto Grab",
            description="Automatically puts a newly created vertex in grab mode",
            default=True)
        extendvert : bpy.props.BoolProperty(
            name="Enable Extend Vert",
    
            description="Enables a way to build tris and quads by adding verts",
    
            default=False)
        quad_from_e_mat : bpy.props.BoolProperty(
            name="Quad From Edge",
            description="Use active material for created face instead of close one",
            default=True)
        quad_from_v_mat : bpy.props.BoolProperty(
            name="Quad From Vert",
            description="Use active material for created face instead of close one",
            default=True)
        tris_from_v_mat : bpy.props.BoolProperty(
            name="Tris From Vert",
            description="Use active material for created face instead of close one",
            default=True)
        ngons_v_mat : bpy.props.BoolProperty(
            name="Ngons",
            description="Use active material for created face instead of close one",
            default=True)
    
    Bart Crouch's avatar
    Bart Crouch committed
    
        def draw(self, context):
            layout = self.layout
    
    
            col = layout.column()
            col.label(text="behaviours:")
            col.prop(self, "autograb")
            col.prop(self, "adjustuv")
            col.prop(self, "extendvert")
    
            col = layout.column()
            col.label(text="use active material when creating:")
            col.prop(self, "quad_from_e_mat")
            col.prop(self, "quad_from_v_mat")
            col.prop(self, "tris_from_v_mat")
            col.prop(self, "ngons_v_mat")
    
    class MeshF2(bpy.types.Operator):
        """Tooltip"""
        bl_idname = "mesh.f2"
        bl_label = "Make Edge/Face"
        bl_description = "Extends the 'Make Edge/Face' functionality"
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context):
            # check we are in mesh editmode
            ob = context.active_object
    
            return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
        def usequad(self, bm, sel, context, event):
            quad_from_vertex(bm, sel, context, event)
            if __name__ != '__main__':
                addon_prefs = context.preferences.addons[__name__].preferences
                if addon_prefs.autograb:
                    bpy.ops.transform.translate('INVOKE_DEFAULT')
    
    
        def invoke(self, context, event):
            bm = bmesh.from_edit_mesh(context.active_object.data)
            sel = [v for v in bm.verts if v.select]
            if len(sel) > 2:
                # original 'Make Edge/Face' behaviour
    
                try:
                    bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
    
                    addon_prefs = context.preferences.addons[__name__].preferences
                    if addon_prefs.ngons_v_mat:
                        bpy.ops.object.material_slot_assign()
    
            elif len(sel) == 1:
                # single vertex selected -> mirror vertex and create new face
    
                addon_prefs = context.preferences.addons[__name__].preferences
                if addon_prefs.extendvert:
                    if checkforconnected('faces') in [2]:
                        if checkforconnected('edges') in [3]:
                            expand_vert(self, context, event)
                        else:
                            self.usequad(bm, sel[0], context, event)
    
                    elif checkforconnected('faces') in [1]:
                        if checkforconnected('edges') in [2]:
                            expand_vert(self, context, event)
                        else:
                            self.usequad(bm, sel[0], context, event)
                    else:
                        self.usequad(bm, sel[0], context, event)
                else:
                    self.usequad(bm, sel[0], context, event)
    
            elif len(sel) == 2:
                edges_sel = [ed for ed in bm.edges if ed.select]
                if len(edges_sel) != 1:
                    # 2 vertices selected, but not on the same edge
                    bpy.ops.mesh.edge_face_add()
                else:
                    # single edge selected -> new face from linked open edges
                    quad_from_edge(bm, edges_sel[0], context, event)
    
            return {'FINISHED'}
    
    
    # registration
    
    classes = [MeshF2, F2AddonPreferences]
    
    addon_keymaps = []
    
    
    def register():
        # add operator
        for c in classes:
            bpy.utils.register_class(c)
    
        # add keymap entry
    
        kcfg = bpy.context.window_manager.keyconfigs.addon
        if kcfg:
            km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
            kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
    
            addon_keymaps.append((km, kmi.idname))
    
        for km, kmi_idname in addon_keymaps:
            for kmi in km.keymap_items:
                if kmi.idname == kmi_idname:
                    km.keymap_items.remove(kmi)
    
    Bart Crouch's avatar
    Bart Crouch committed
        # remove operator and preferences
        for c in reversed(classes):
            bpy.utils.unregister_class(c)