1# Updated for 2.8 jan 5 2019
2
3# ##### BEGIN GPL LICENSE BLOCK #####
4#
5#  This program is free software; you can redistribute it and/or
6#  modify it under the terms of the GNU General Public License
7#  as published by the Free Software Foundation; either version 2
8#  of the License, or (at your option) any later version.
9#
10#  This program is distributed in the hope that it will be useful,
11#  but WITHOUT ANY WARRANTY; without even the implied warranty of
12#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#  GNU General Public License for more details.
14#
15#  You should have received a copy of the GNU General Public License
16#  along with this program; if not, write to the Free Software Foundation,
17#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18#
19# ##### END GPL LICENSE BLOCK #####
20
21# <pep8 compliant>
22
23bl_info = {
24    "name": "F2",
25    "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
26              "(concept design), Adrian Rutkowski",
27    "version": (1, 8, 4),
28    "blender": (2, 80, 0),
29    "location": "Editmode > F",
30    "warning": "",
31    "description": "Extends the 'Make Edge/Face' functionality",
32    "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/f2.html",
33    "category": "Mesh",
34}
35
36# ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
37
38import bmesh
39import bpy
40import itertools
41import mathutils
42import math
43from mathutils import Vector
44from bpy_extras import view3d_utils
45
46
47# returns a custom data layer of the UV map, or None
48def get_uv_layer(ob, bm, mat_index):
49    uv = None
50    uv_layer = None
51    if ob.material_slots:
52        me = ob.data
53        if me.uv_layers:
54            uv = me.uv_layers.active.name
55    # 'material_slots' is deprecated (Blender Internal)
56    # else:
57    #     mat = ob.material_slots[mat_index].material
58    #     if mat is not None:
59    #         slot = mat.texture_slots[mat.active_texture_index]
60    #         if slot and slot.uv_layer:
61    #             uv = slot.uv_layer
62    #         else:
63    #             for tex_slot in mat.texture_slots:
64    #                 if tex_slot and tex_slot.uv_layer:
65    #                     uv = tex_slot.uv_layer
66    #                     break
67    if uv:
68        uv_layer = bm.loops.layers.uv.get(uv)
69
70    return (uv_layer)
71
72
73# create a face from a single selected edge
74def quad_from_edge(bm, edge_sel, context, event):
75    addon_prefs = context.preferences.addons[__name__].preferences
76    ob = context.active_object
77    region = context.region
78    region_3d = context.space_data.region_3d
79
80    # find linked edges that are open (<2 faces connected) and not part of
81    # the face the selected edge belongs to
82    all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
83                  len(edge.link_faces) < 2 and edge != edge_sel and \
84                  sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
85                 for i in range(2)]
86    if not all_edges[0] or not all_edges[1]:
87        return
88
89    # determine which edges to use, based on mouse cursor position
90    mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
91    optimal_edges = []
92    for edges in all_edges:
93        min_dist = False
94        for edge in edges:
95            vert = [vert for vert in edge.verts if not vert.select][0]
96            world_pos = ob.matrix_world @ vert.co.copy()
97            screen_pos = view3d_utils.location_3d_to_region_2d(region,
98                                                               region_3d, world_pos)
99            dist = (mouse_pos - screen_pos).length
100            if not min_dist or dist < min_dist[0]:
101                min_dist = (dist, edge, vert)
102        optimal_edges.append(min_dist)
103
104    # determine the vertices, which make up the quad
105    v1 = edge_sel.verts[0]
106    v2 = edge_sel.verts[1]
107    edge_1 = optimal_edges[0][1]
108    edge_2 = optimal_edges[1][1]
109    v3 = optimal_edges[0][2]
110    v4 = optimal_edges[1][2]
111
112    # normal detection
113    flip_align = True
114    normal_edge = edge_1
115    if not normal_edge.link_faces:
116        normal_edge = edge_2
117        if not normal_edge.link_faces:
118            normal_edge = edge_sel
119            if not normal_edge.link_faces:
120                # no connected faces, so no need to flip the face normal
121                flip_align = False
122    if flip_align:  # there is a face to which the normal can be aligned
123        ref_verts = [v for v in normal_edge.link_faces[0].verts]
124        if v3 in ref_verts and v1 in ref_verts:
125            va_1 = v3
126            va_2 = v1
127        elif normal_edge == edge_sel:
128            va_1 = v1
129            va_2 = v2
130        else:
131            va_1 = v2
132            va_2 = v4
133        if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
134                (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
135            # reference verts are at start and end of the list -> shift list
136            ref_verts = ref_verts[1:] + [ref_verts[0]]
137        if ref_verts.index(va_1) > ref_verts.index(va_2):
138            # connected face has same normal direction, so don't flip
139            flip_align = False
140
141    # material index detection
142    ref_faces = edge_sel.link_faces
143    if not ref_faces:
144        ref_faces = edge_sel.verts[0].link_faces
145    if not ref_faces:
146        ref_faces = edge_sel.verts[1].link_faces
147    if not ref_faces:
148        mat_index = False
149        smooth = False
150    else:
151        mat_index = ref_faces[0].material_index
152        smooth = ref_faces[0].smooth
153
154    if addon_prefs.quad_from_e_mat:
155        mat_index = bpy.context.object.active_material_index
156
157    # create quad
158    try:
159        if v3 == v4:
160            # triangle (usually at end of quad-strip
161            verts = [v3, v1, v2]
162        else:
163            # normal face creation
164            verts = [v3, v1, v2, v4]
165        if flip_align:
166            verts.reverse()
167        face = bm.faces.new(verts)
168        if mat_index:
169            face.material_index = mat_index
170        face.smooth = smooth
171    except:
172        # face already exists
173        return
174
175    # change selection
176    edge_sel.select = False
177    for vert in edge_sel.verts:
178        vert.select = False
179    for edge in face.edges:
180        if edge.index < 0:
181            edge.select = True
182    v3.select = True
183    v4.select = True
184
185    # adjust uv-map
186    if __name__ != '__main__':
187        if addon_prefs.adjustuv:
188            uv_layer = get_uv_layer(ob, bm, mat_index)
189            if uv_layer:
190                uv_ori = {}
191                for vert in [v1, v2, v3, v4]:
192                    for loop in vert.link_loops:
193                        if loop.face.index > -1:
194                            uv_ori[loop.vert.index] = loop[uv_layer].uv
195                if len(uv_ori) == 4 or len(uv_ori) == 3:
196                    for loop in face.loops:
197                        if loop.vert.index in uv_ori:
198                            loop[uv_layer].uv = uv_ori[loop.vert.index]
199
200    # toggle mode, to force correct drawing
201    bpy.ops.object.mode_set(mode='OBJECT')
202    bpy.ops.object.mode_set(mode='EDIT')
203
204
205# create a face from a single selected vertex, if it is an open vertex
206def quad_from_vertex(bm, vert_sel, context, event):
207    addon_prefs = context.preferences.addons[__name__].preferences
208    ob = context.active_object
209    me = ob.data
210    region = context.region
211    region_3d = context.space_data.region_3d
212
213    # find linked edges that are open (<2 faces connected)
214    edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
215    if len(edges) < 2:
216        return
217
218    # determine which edges to use, based on mouse cursor position
219    min_dist = False
220    mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
221    for a, b in itertools.combinations(edges, 2):
222        other_verts = [vert for edge in [a, b] for vert in edge.verts \
223                       if not vert.select]
224        mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
225                    / 2
226        new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
227        world_pos = ob.matrix_world @ new_pos
228        screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
229                                                           world_pos)
230        dist = (mouse_pos - screen_pos).length
231        if not min_dist or dist < min_dist[0]:
232            min_dist = (dist, (a, b), other_verts, new_pos)
233
234    # create vertex at location mirrored in the line, connecting the open edges
235    edges = min_dist[1]
236    other_verts = min_dist[2]
237    new_pos = min_dist[3]
238    vert_new = bm.verts.new(new_pos)
239
240    # normal detection
241    flip_align = True
242    normal_edge = edges[0]
243    if not normal_edge.link_faces:
244        normal_edge = edges[1]
245        if not normal_edge.link_faces:
246            # no connected faces, so no need to flip the face normal
247            flip_align = False
248    if flip_align:  # there is a face to which the normal can be aligned
249        ref_verts = [v for v in normal_edge.link_faces[0].verts]
250        if other_verts[0] in ref_verts:
251            va_1 = other_verts[0]
252            va_2 = vert_sel
253        else:
254            va_1 = vert_sel
255            va_2 = other_verts[1]
256        if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
257                (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
258            # reference verts are at start and end of the list -> shift list
259            ref_verts = ref_verts[1:] + [ref_verts[0]]
260        if ref_verts.index(va_1) > ref_verts.index(va_2):
261            # connected face has same normal direction, so don't flip
262            flip_align = False
263
264    # material index detection
265    ref_faces = vert_sel.link_faces
266    if not ref_faces:
267        mat_index = False
268        smooth = False
269    else:
270        mat_index = ref_faces[0].material_index
271        smooth = ref_faces[0].smooth
272
273    if addon_prefs.quad_from_v_mat:
274        mat_index = bpy.context.object.active_material_index
275
276    # create face between all 4 vertices involved
277    verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
278    if flip_align:
279        verts.reverse()
280    face = bm.faces.new(verts)
281    if mat_index:
282        face.material_index = mat_index
283    face.smooth = smooth
284
285    # change selection
286    vert_new.select = True
287    vert_sel.select = False
288
289    # adjust uv-map
290    if __name__ != '__main__':
291        if addon_prefs.adjustuv:
292            uv_layer = get_uv_layer(ob, bm, mat_index)
293            if uv_layer:
294                uv_others = {}
295                uv_sel = None
296                uv_new = None
297                # get original uv coordinates
298                for i in range(2):
299                    for loop in other_verts[i].link_loops:
300                        if loop.face.index > -1:
301                            uv_others[loop.vert.index] = loop[uv_layer].uv
302                            break
303                if len(uv_others) == 2:
304                    mid_other = (list(uv_others.values())[0] +
305                                 list(uv_others.values())[1]) / 2
306                    for loop in vert_sel.link_loops:
307                        if loop.face.index > -1:
308                            uv_sel = loop[uv_layer].uv
309                            break
310                    if uv_sel:
311                        uv_new = 2 * (mid_other - uv_sel) + uv_sel
312
313                # set uv coordinates for new loops
314                if uv_new:
315                    for loop in face.loops:
316                        if loop.vert.index == -1:
317                            x, y = uv_new
318                        elif loop.vert.index in uv_others:
319                            x, y = uv_others[loop.vert.index]
320                        else:
321                            x, y = uv_sel
322                        loop[uv_layer].uv = (x, y)
323
324    # toggle mode, to force correct drawing
325    bpy.ops.object.mode_set(mode='OBJECT')
326    bpy.ops.object.mode_set(mode='EDIT')
327
328
329def expand_vert(self, context, event):
330    addon_prefs = context.preferences.addons[__name__].preferences
331    ob = context.active_object
332    obj = bpy.context.object
333    me = obj.data
334    bm = bmesh.from_edit_mesh(me)
335    region = context.region
336    region_3d = context.space_data.region_3d
337    rv3d = context.space_data.region_3d
338
339    for v in bm.verts:
340        if v.select:
341            v_active = v
342
343    try:
344        depth_location = v_active.co
345    except:
346        return {'CANCELLED'}
347    # create vert in mouse cursor location
348
349    mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
350    location_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, depth_location)
351
352    c_verts = []
353    # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
354    linked = v_active.link_edges
355    for edges in linked:
356        if len(edges.link_faces) < 2:
357            edges.select = True
358            for v in edges.verts:
359                if v is not v_active:
360                    c_verts.append(v)
361
362    # Compare distance in 2d between mouse and edges middle points
363    screen_pos_va = view3d_utils.location_3d_to_region_2d(region, region_3d,
364                                                          ob.matrix_world @ v_active.co)
365    screen_pos_v1 = view3d_utils.location_3d_to_region_2d(region, region_3d,
366                                                          ob.matrix_world @ c_verts[0].co)
367    screen_pos_v2 = view3d_utils.location_3d_to_region_2d(region, region_3d,
368                                                          ob.matrix_world @ c_verts[1].co)
369
370    mid_pos_v1 = Vector(((screen_pos_va[0] + screen_pos_v1[0]) / 2, (screen_pos_va[1] + screen_pos_v1[1]) / 2))
371    mid_pos_V2 = Vector(((screen_pos_va[0] + screen_pos_v2[0]) / 2, (screen_pos_va[1] + screen_pos_v2[1]) / 2))
372
373    dist1 = math.log10(pow((mid_pos_v1[0] - mouse_pos[0]), 2) + pow((mid_pos_v1[1] - mouse_pos[1]), 2))
374    dist2 = math.log10(pow((mid_pos_V2[0] - mouse_pos[0]), 2) + pow((mid_pos_V2[1] - mouse_pos[1]), 2))
375
376    bm.normal_update()
377    bm.verts.ensure_lookup_table()
378
379    # Deselect not needed point and create new face
380    if dist1 < dist2:
381        c_verts[1].select = False
382        lleft = c_verts[0].link_faces
383
384    else:
385        c_verts[0].select = False
386        lleft = c_verts[1].link_faces
387
388    lactive = v_active.link_faces
389    # lverts = lactive[0].verts
390
391    mat_index = lactive[0].material_index
392    smooth = lactive[0].smooth
393
394    for faces in lactive:
395        if faces in lleft:
396            cface = faces
397            if len(faces.verts) == 3:
398                bm.normal_update()
399                bmesh.update_edit_mesh(obj.data)
400                bpy.ops.mesh.select_all(action='DESELECT')
401                v_active.select = True
402                bpy.ops.mesh.rip_edge_move('INVOKE_DEFAULT')
403                return {'FINISHED'}
404
405    lverts = cface.verts
406
407    # create triangle with correct normal orientation
408    # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...
409
410    # from L to R
411    if dist1 < dist2:
412        if (lverts[0] == v_active and lverts[3] == c_verts[0]) \
413                or (lverts[2] == v_active and lverts[1] == c_verts[0]) \
414                or (lverts[1] == v_active and lverts[0] == c_verts[0]) \
415                or (lverts[3] == v_active and lverts[2] == c_verts[0]):
416            v_new = bm.verts.new(v_active.co)
417            face_new = bm.faces.new((c_verts[0], v_new, v_active))
418
419        elif (lverts[1] == v_active and lverts[2] == c_verts[0]) \
420                or (lverts[0] == v_active and lverts[1] == c_verts[0]) \
421                or (lverts[3] == v_active and lverts[0] == c_verts[0]) \
422                or (lverts[2] == v_active and lverts[3] == c_verts[0]):
423            v_new = bm.verts.new(v_active.co)
424            face_new = bm.faces.new((v_active, v_new, c_verts[0]))
425
426        else:
427            pass
428    # from R to L
429    else:
430        if (lverts[2] == v_active and lverts[3] == c_verts[1]) \
431                or (lverts[0] == v_active and lverts[1] == c_verts[1]) \
432                or (lverts[1] == v_active and lverts[2] == c_verts[1]) \
433                or (lverts[3] == v_active and lverts[0] == c_verts[1]):
434            v_new = bm.verts.new(v_active.co)
435            face_new = bm.faces.new((v_active, v_new, c_verts[1]))
436
437        elif (lverts[0] == v_active and lverts[3] == c_verts[1]) \
438                or (lverts[2] == v_active and lverts[1] == c_verts[1]) \
439                or (lverts[1] == v_active and lverts[0] == c_verts[1]) \
440                or (lverts[3] == v_active and lverts[2] == c_verts[1]):
441            v_new = bm.verts.new(v_active.co)
442            face_new = bm.faces.new((c_verts[1], v_new, v_active))
443
444        else:
445            pass
446
447    # set smooth and mat based on starting face
448    if addon_prefs.tris_from_v_mat:
449        face_new.material_index = bpy.context.object.active_material_index
450    else:
451        face_new.material_index = mat_index
452    face_new.smooth = smooth
453
454    # update normals
455    bpy.ops.mesh.select_all(action='DESELECT')
456    v_new.select = True
457    bm.select_history.add(v_new)
458
459    bm.normal_update()
460    bmesh.update_edit_mesh(obj.data)
461    bpy.ops.transform.translate('INVOKE_DEFAULT')
462
463
464def checkforconnected(conection):
465    obj = bpy.context.object
466    me = obj.data
467    bm = bmesh.from_edit_mesh(me)
468
469    # Checks for number of edes or faces connected to selected vertex
470    for v in bm.verts:
471        if v.select:
472            v_active = v
473    if conection == 'faces':
474        linked = v_active.link_faces
475    elif conection == 'edges':
476        linked = v_active.link_edges
477
478    bmesh.update_edit_mesh(obj.data)
479    return len(linked)
480
481
482# autograb preference in addons panel
483class F2AddonPreferences(bpy.types.AddonPreferences):
484    bl_idname = __name__
485    adjustuv : bpy.props.BoolProperty(
486        name="Adjust UV",
487        description="Automatically update UV unwrapping",
488        default=False)
489    autograb : bpy.props.BoolProperty(
490        name="Auto Grab",
491        description="Automatically puts a newly created vertex in grab mode",
492        default=True)
493    extendvert : bpy.props.BoolProperty(
494        name="Enable Extend Vert",
495        description="Enables a way to build tris and quads by adding verts",
496        default=False)
497    quad_from_e_mat : bpy.props.BoolProperty(
498        name="Quad From Edge",
499        description="Use active material for created face instead of close one",
500        default=True)
501    quad_from_v_mat : bpy.props.BoolProperty(
502        name="Quad From Vert",
503        description="Use active material for created face instead of close one",
504        default=True)
505    tris_from_v_mat : bpy.props.BoolProperty(
506        name="Tris From Vert",
507        description="Use active material for created face instead of close one",
508        default=True)
509    ngons_v_mat : bpy.props.BoolProperty(
510        name="Ngons",
511        description="Use active material for created face instead of close one",
512        default=True)
513
514    def draw(self, context):
515        layout = self.layout
516
517        col = layout.column()
518        col.label(text="behaviours:")
519        col.prop(self, "autograb")
520        col.prop(self, "adjustuv")
521        col.prop(self, "extendvert")
522
523        col = layout.column()
524        col.label(text="use active material when creating:")
525        col.prop(self, "quad_from_e_mat")
526        col.prop(self, "quad_from_v_mat")
527        col.prop(self, "tris_from_v_mat")
528        col.prop(self, "ngons_v_mat")
529
530
531class MeshF2(bpy.types.Operator):
532    """Tooltip"""
533    bl_idname = "mesh.f2"
534    bl_label = "Make Edge/Face"
535    bl_description = "Extends the 'Make Edge/Face' functionality"
536    bl_options = {'REGISTER', 'UNDO'}
537
538    @classmethod
539    def poll(cls, context):
540        # check we are in mesh editmode
541        ob = context.active_object
542        return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
543
544    def usequad(self, bm, sel, context, event):
545        quad_from_vertex(bm, sel, context, event)
546        if __name__ != '__main__':
547            addon_prefs = context.preferences.addons[__name__].preferences
548            if addon_prefs.autograb:
549                bpy.ops.transform.translate('INVOKE_DEFAULT')
550
551    def invoke(self, context, event):
552        bm = bmesh.from_edit_mesh(context.active_object.data)
553        sel = [v for v in bm.verts if v.select]
554        if len(sel) > 2:
555            # original 'Make Edge/Face' behaviour
556            try:
557                bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
558                addon_prefs = context.preferences.addons[__name__].preferences
559                if addon_prefs.ngons_v_mat:
560                    bpy.ops.object.material_slot_assign()
561            except:
562                return {'CANCELLED'}
563        elif len(sel) == 1:
564            # single vertex selected -> mirror vertex and create new face
565            addon_prefs = context.preferences.addons[__name__].preferences
566            if addon_prefs.extendvert:
567                if checkforconnected('faces') in [2]:
568                    if checkforconnected('edges') in [3]:
569                        expand_vert(self, context, event)
570                    else:
571                        self.usequad(bm, sel[0], context, event)
572
573                elif checkforconnected('faces') in [1]:
574                    if checkforconnected('edges') in [2]:
575                        expand_vert(self, context, event)
576                    else:
577                        self.usequad(bm, sel[0], context, event)
578                else:
579                    self.usequad(bm, sel[0], context, event)
580            else:
581                self.usequad(bm, sel[0], context, event)
582        elif len(sel) == 2:
583            edges_sel = [ed for ed in bm.edges if ed.select]
584            if len(edges_sel) != 1:
585                # 2 vertices selected, but not on the same edge
586                bpy.ops.mesh.edge_face_add()
587            else:
588                # single edge selected -> new face from linked open edges
589                quad_from_edge(bm, edges_sel[0], context, event)
590
591        return {'FINISHED'}
592
593
594# registration
595classes = [MeshF2, F2AddonPreferences]
596addon_keymaps = []
597
598
599def register():
600    # add operator
601    for c in classes:
602        bpy.utils.register_class(c)
603
604    # add keymap entry
605    kcfg = bpy.context.window_manager.keyconfigs.addon
606    if kcfg:
607        km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
608        kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
609        addon_keymaps.append((km, kmi.idname))
610
611
612def unregister():
613    # remove keymap entry
614    for km, kmi_idname in addon_keymaps:
615        for kmi in km.keymap_items:
616            if kmi.idname == kmi_idname:
617                km.keymap_items.remove(kmi)
618    addon_keymaps.clear()
619
620    # remove operator and preferences
621    for c in reversed(classes):
622        bpy.utils.unregister_class(c)
623
624
625if __name__ == "__main__":
626    register()
627