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