1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Updated for 2.8 jan 5 2019
9 "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
10 "(concept design), Adrian Rutkowski",
12 "blender": (2, 80, 0),
13 "location": "Editmode > F",
15 "description": "Extends the 'Make Edge/Face' functionality",
16 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/f2.html",
20 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
27 from mathutils
import Vector
28 from bpy_extras
import view3d_utils
31 # returns a custom data layer of the UV map, or None
32 def get_uv_layer(ob
, bm
, mat_index
):
38 uv
= me
.uv_layers
.active
.name
39 # 'material_slots' is deprecated (Blender Internal)
41 # mat = ob.material_slots[mat_index].material
43 # slot = mat.texture_slots[mat.active_texture_index]
44 # if slot and slot.uv_layer:
47 # for tex_slot in mat.texture_slots:
48 # if tex_slot and tex_slot.uv_layer:
49 # uv = tex_slot.uv_layer
52 uv_layer
= bm
.loops
.layers
.uv
.get(uv
)
57 # create a face from a single selected edge
58 def quad_from_edge(bm
, edge_sel
, context
, event
):
59 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
60 ob
= context
.active_object
61 region
= context
.region
62 region_3d
= context
.space_data
.region_3d
64 # find linked edges that are open (<2 faces connected) and not part of
65 # the face the selected edge belongs to
66 all_edges
= [[edge
for edge
in edge_sel
.verts
[i
].link_edges
if \
67 len(edge
.link_faces
) < 2 and edge
!= edge_sel
and \
68 sum([face
in edge_sel
.link_faces
for face
in edge
.link_faces
]) == 0] \
70 if not all_edges
[0] or not all_edges
[1]:
73 # determine which edges to use, based on mouse cursor position
74 mouse_pos
= mathutils
.Vector([event
.mouse_region_x
, event
.mouse_region_y
])
76 for edges
in all_edges
:
79 vert
= [vert
for vert
in edge
.verts
if not vert
.select
][0]
80 world_pos
= ob
.matrix_world
@ vert
.co
.copy()
81 screen_pos
= view3d_utils
.location_3d_to_region_2d(region
,
83 dist
= (mouse_pos
- screen_pos
).length
84 if not min_dist
or dist
< min_dist
[0]:
85 min_dist
= (dist
, edge
, vert
)
86 optimal_edges
.append(min_dist
)
88 # determine the vertices, which make up the quad
89 v1
= edge_sel
.verts
[0]
90 v2
= edge_sel
.verts
[1]
91 edge_1
= optimal_edges
[0][1]
92 edge_2
= optimal_edges
[1][1]
93 v3
= optimal_edges
[0][2]
94 v4
= optimal_edges
[1][2]
99 if not normal_edge
.link_faces
:
101 if not normal_edge
.link_faces
:
102 normal_edge
= edge_sel
103 if not normal_edge
.link_faces
:
104 # no connected faces, so no need to flip the face normal
106 if flip_align
: # there is a face to which the normal can be aligned
107 ref_verts
= [v
for v
in normal_edge
.link_faces
[0].verts
]
108 if v3
in ref_verts
and v1
in ref_verts
:
111 elif normal_edge
== edge_sel
:
117 if (va_1
== ref_verts
[0] and va_2
== ref_verts
[-1]) or \
118 (va_2
== ref_verts
[0] and va_1
== ref_verts
[-1]):
119 # reference verts are at start and end of the list -> shift list
120 ref_verts
= ref_verts
[1:] + [ref_verts
[0]]
121 if ref_verts
.index(va_1
) > ref_verts
.index(va_2
):
122 # connected face has same normal direction, so don't flip
125 # material index detection
126 ref_faces
= edge_sel
.link_faces
128 ref_faces
= edge_sel
.verts
[0].link_faces
130 ref_faces
= edge_sel
.verts
[1].link_faces
135 mat_index
= ref_faces
[0].material_index
136 smooth
= ref_faces
[0].smooth
138 if addon_prefs
.quad_from_e_mat
:
139 mat_index
= bpy
.context
.object.active_material_index
144 # triangle (usually at end of quad-strip
147 # normal face creation
148 verts
= [v3
, v1
, v2
, v4
]
151 face
= bm
.faces
.new(verts
)
153 face
.material_index
= mat_index
156 # face already exists
160 edge_sel
.select
= False
161 for vert
in edge_sel
.verts
:
163 for edge
in face
.edges
:
170 if __name__
!= '__main__':
171 if addon_prefs
.adjustuv
:
172 uv_layer
= get_uv_layer(ob
, bm
, mat_index
)
175 for vert
in [v1
, v2
, v3
, v4
]:
176 for loop
in vert
.link_loops
:
177 if loop
.face
.index
> -1:
178 uv_ori
[loop
.vert
.index
] = loop
[uv_layer
].uv
179 if len(uv_ori
) == 4 or len(uv_ori
) == 3:
180 for loop
in face
.loops
:
181 if loop
.vert
.index
in uv_ori
:
182 loop
[uv_layer
].uv
= uv_ori
[loop
.vert
.index
]
184 # toggle mode, to force correct drawing
185 bpy
.ops
.object.mode_set(mode
='OBJECT')
186 bpy
.ops
.object.mode_set(mode
='EDIT')
189 # create a face from a single selected vertex, if it is an open vertex
190 def quad_from_vertex(bm
, vert_sel
, context
, event
):
191 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
192 ob
= context
.active_object
194 region
= context
.region
195 region_3d
= context
.space_data
.region_3d
197 # find linked edges that are open (<2 faces connected)
198 edges
= [edge
for edge
in vert_sel
.link_edges
if len(edge
.link_faces
) < 2]
202 # determine which edges to use, based on mouse cursor position
204 mouse_pos
= mathutils
.Vector([event
.mouse_region_x
, event
.mouse_region_y
])
205 for a
, b
in itertools
.combinations(edges
, 2):
206 other_verts
= [vert
for edge
in [a
, b
] for vert
in edge
.verts \
208 mid_other
= (other_verts
[0].co
.copy() + other_verts
[1].co
.copy()) \
210 new_pos
= 2 * (mid_other
- vert_sel
.co
.copy()) + vert_sel
.co
.copy()
211 world_pos
= ob
.matrix_world
@ new_pos
212 screen_pos
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
214 dist
= (mouse_pos
- screen_pos
).length
215 if not min_dist
or dist
< min_dist
[0]:
216 min_dist
= (dist
, (a
, b
), other_verts
, new_pos
)
218 # create vertex at location mirrored in the line, connecting the open edges
220 other_verts
= min_dist
[2]
221 new_pos
= min_dist
[3]
222 vert_new
= bm
.verts
.new(new_pos
)
226 normal_edge
= edges
[0]
227 if not normal_edge
.link_faces
:
228 normal_edge
= edges
[1]
229 if not normal_edge
.link_faces
:
230 # no connected faces, so no need to flip the face normal
232 if flip_align
: # there is a face to which the normal can be aligned
233 ref_verts
= [v
for v
in normal_edge
.link_faces
[0].verts
]
234 if other_verts
[0] in ref_verts
:
235 va_1
= other_verts
[0]
239 va_2
= other_verts
[1]
240 if (va_1
== ref_verts
[0] and va_2
== ref_verts
[-1]) or \
241 (va_2
== ref_verts
[0] and va_1
== ref_verts
[-1]):
242 # reference verts are at start and end of the list -> shift list
243 ref_verts
= ref_verts
[1:] + [ref_verts
[0]]
244 if ref_verts
.index(va_1
) > ref_verts
.index(va_2
):
245 # connected face has same normal direction, so don't flip
248 # material index detection
249 ref_faces
= vert_sel
.link_faces
254 mat_index
= ref_faces
[0].material_index
255 smooth
= ref_faces
[0].smooth
257 if addon_prefs
.quad_from_v_mat
:
258 mat_index
= bpy
.context
.object.active_material_index
260 # create face between all 4 vertices involved
261 verts
= [other_verts
[0], vert_sel
, other_verts
[1], vert_new
]
264 face
= bm
.faces
.new(verts
)
266 face
.material_index
= mat_index
270 vert_new
.select
= True
271 vert_sel
.select
= False
274 if __name__
!= '__main__':
275 if addon_prefs
.adjustuv
:
276 uv_layer
= get_uv_layer(ob
, bm
, mat_index
)
281 # get original uv coordinates
283 for loop
in other_verts
[i
].link_loops
:
284 if loop
.face
.index
> -1:
285 uv_others
[loop
.vert
.index
] = loop
[uv_layer
].uv
287 if len(uv_others
) == 2:
288 mid_other
= (list(uv_others
.values())[0] +
289 list(uv_others
.values())[1]) / 2
290 for loop
in vert_sel
.link_loops
:
291 if loop
.face
.index
> -1:
292 uv_sel
= loop
[uv_layer
].uv
295 uv_new
= 2 * (mid_other
- uv_sel
) + uv_sel
297 # set uv coordinates for new loops
299 for loop
in face
.loops
:
300 if loop
.vert
.index
== -1:
302 elif loop
.vert
.index
in uv_others
:
303 x
, y
= uv_others
[loop
.vert
.index
]
306 loop
[uv_layer
].uv
= (x
, y
)
308 # toggle mode, to force correct drawing
309 bpy
.ops
.object.mode_set(mode
='OBJECT')
310 bpy
.ops
.object.mode_set(mode
='EDIT')
313 def expand_vert(self
, context
, event
):
314 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
315 ob
= context
.active_object
316 obj
= bpy
.context
.object
318 bm
= bmesh
.from_edit_mesh(me
)
319 region
= context
.region
320 region_3d
= context
.space_data
.region_3d
321 rv3d
= context
.space_data
.region_3d
328 depth_location
= v_active
.co
331 # create vert in mouse cursor location
333 mouse_pos
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
334 location_3d
= view3d_utils
.region_2d_to_location_3d(region
, rv3d
, mouse_pos
, depth_location
)
337 # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
338 linked
= v_active
.link_edges
340 if len(edges
.link_faces
) < 2:
342 for v
in edges
.verts
:
343 if v
is not v_active
:
346 # Compare distance in 2d between mouse and edges middle points
347 screen_pos_va
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
348 ob
.matrix_world
@ v_active
.co
)
349 screen_pos_v1
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
350 ob
.matrix_world
@ c_verts
[0].co
)
351 screen_pos_v2
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
352 ob
.matrix_world
@ c_verts
[1].co
)
354 mid_pos_v1
= Vector(((screen_pos_va
[0] + screen_pos_v1
[0]) / 2, (screen_pos_va
[1] + screen_pos_v1
[1]) / 2))
355 mid_pos_V2
= Vector(((screen_pos_va
[0] + screen_pos_v2
[0]) / 2, (screen_pos_va
[1] + screen_pos_v2
[1]) / 2))
357 dist1
= math
.log10(pow((mid_pos_v1
[0] - mouse_pos
[0]), 2) + pow((mid_pos_v1
[1] - mouse_pos
[1]), 2))
358 dist2
= math
.log10(pow((mid_pos_V2
[0] - mouse_pos
[0]), 2) + pow((mid_pos_V2
[1] - mouse_pos
[1]), 2))
361 bm
.verts
.ensure_lookup_table()
363 # Deselect not needed point and create new face
365 c_verts
[1].select
= False
366 lleft
= c_verts
[0].link_faces
369 c_verts
[0].select
= False
370 lleft
= c_verts
[1].link_faces
372 lactive
= v_active
.link_faces
373 # lverts = lactive[0].verts
375 mat_index
= lactive
[0].material_index
376 smooth
= lactive
[0].smooth
378 for faces
in lactive
:
381 if len(faces
.verts
) == 3:
383 bmesh
.update_edit_mesh(obj
.data
)
384 bpy
.ops
.mesh
.select_all(action
='DESELECT')
385 v_active
.select
= True
386 bpy
.ops
.mesh
.rip_edge_move('INVOKE_DEFAULT')
391 # create triangle with correct normal orientation
392 # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...
396 if (lverts
[0] == v_active
and lverts
[3] == c_verts
[0]) \
397 or (lverts
[2] == v_active
and lverts
[1] == c_verts
[0]) \
398 or (lverts
[1] == v_active
and lverts
[0] == c_verts
[0]) \
399 or (lverts
[3] == v_active
and lverts
[2] == c_verts
[0]):
400 v_new
= bm
.verts
.new(v_active
.co
)
401 face_new
= bm
.faces
.new((c_verts
[0], v_new
, v_active
))
403 elif (lverts
[1] == v_active
and lverts
[2] == c_verts
[0]) \
404 or (lverts
[0] == v_active
and lverts
[1] == c_verts
[0]) \
405 or (lverts
[3] == v_active
and lverts
[0] == c_verts
[0]) \
406 or (lverts
[2] == v_active
and lverts
[3] == c_verts
[0]):
407 v_new
= bm
.verts
.new(v_active
.co
)
408 face_new
= bm
.faces
.new((v_active
, v_new
, c_verts
[0]))
414 if (lverts
[2] == v_active
and lverts
[3] == c_verts
[1]) \
415 or (lverts
[0] == v_active
and lverts
[1] == c_verts
[1]) \
416 or (lverts
[1] == v_active
and lverts
[2] == c_verts
[1]) \
417 or (lverts
[3] == v_active
and lverts
[0] == c_verts
[1]):
418 v_new
= bm
.verts
.new(v_active
.co
)
419 face_new
= bm
.faces
.new((v_active
, v_new
, c_verts
[1]))
421 elif (lverts
[0] == v_active
and lverts
[3] == c_verts
[1]) \
422 or (lverts
[2] == v_active
and lverts
[1] == c_verts
[1]) \
423 or (lverts
[1] == v_active
and lverts
[0] == c_verts
[1]) \
424 or (lverts
[3] == v_active
and lverts
[2] == c_verts
[1]):
425 v_new
= bm
.verts
.new(v_active
.co
)
426 face_new
= bm
.faces
.new((c_verts
[1], v_new
, v_active
))
431 # set smooth and mat based on starting face
432 if addon_prefs
.tris_from_v_mat
:
433 face_new
.material_index
= bpy
.context
.object.active_material_index
435 face_new
.material_index
= mat_index
436 face_new
.smooth
= smooth
439 bpy
.ops
.mesh
.select_all(action
='DESELECT')
441 bm
.select_history
.add(v_new
)
444 bmesh
.update_edit_mesh(obj
.data
)
445 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
448 def checkforconnected(connection
):
449 obj
= bpy
.context
.object
451 bm
= bmesh
.from_edit_mesh(me
)
453 # Checks for number of edes or faces connected to selected vertex
457 if connection
== 'faces':
458 linked
= v_active
.link_faces
459 elif connection
== 'edges':
460 linked
= v_active
.link_edges
462 bmesh
.update_edit_mesh(obj
.data
)
466 # autograb preference in addons panel
467 class F2AddonPreferences(bpy
.types
.AddonPreferences
):
469 adjustuv
: bpy
.props
.BoolProperty(
471 description
="Automatically update UV unwrapping",
473 autograb
: bpy
.props
.BoolProperty(
475 description
="Automatically puts a newly created vertex in grab mode",
477 extendvert
: bpy
.props
.BoolProperty(
478 name
="Enable Extend Vert",
479 description
="Enables a way to build tris and quads by adding verts",
481 quad_from_e_mat
: bpy
.props
.BoolProperty(
482 name
="Quad From Edge",
483 description
="Use active material for created face instead of close one",
485 quad_from_v_mat
: bpy
.props
.BoolProperty(
486 name
="Quad From Vert",
487 description
="Use active material for created face instead of close one",
489 tris_from_v_mat
: bpy
.props
.BoolProperty(
490 name
="Tris From Vert",
491 description
="Use active material for created face instead of close one",
493 ngons_v_mat
: bpy
.props
.BoolProperty(
495 description
="Use active material for created face instead of close one",
498 def draw(self
, context
):
501 col
= layout
.column()
502 col
.label(text
="behaviours:")
503 col
.prop(self
, "autograb")
504 col
.prop(self
, "adjustuv")
505 col
.prop(self
, "extendvert")
507 col
= layout
.column()
508 col
.label(text
="use active material when creating:")
509 col
.prop(self
, "quad_from_e_mat")
510 col
.prop(self
, "quad_from_v_mat")
511 col
.prop(self
, "tris_from_v_mat")
512 col
.prop(self
, "ngons_v_mat")
515 class MeshF2(bpy
.types
.Operator
):
517 bl_idname
= "mesh.f2"
518 bl_label
= "Make Edge/Face"
519 bl_description
= "Extends the 'Make Edge/Face' functionality"
520 bl_options
= {'REGISTER', 'UNDO'}
523 def poll(cls
, context
):
524 # check we are in mesh editmode
525 ob
= context
.active_object
526 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
528 def usequad(self
, bm
, sel
, context
, event
):
529 quad_from_vertex(bm
, sel
, context
, event
)
530 if __name__
!= '__main__':
531 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
532 if addon_prefs
.autograb
:
533 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
535 def invoke(self
, context
, event
):
536 bm
= bmesh
.from_edit_mesh(context
.active_object
.data
)
537 sel
= [v
for v
in bm
.verts
if v
.select
]
539 # original 'Make Edge/Face' behaviour
541 bpy
.ops
.mesh
.edge_face_add('INVOKE_DEFAULT')
542 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
543 if addon_prefs
.ngons_v_mat
:
544 bpy
.ops
.object.material_slot_assign()
548 # single vertex selected -> mirror vertex and create new face
549 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
550 if addon_prefs
.extendvert
:
551 if checkforconnected('faces') in [2]:
552 if checkforconnected('edges') in [3]:
553 expand_vert(self
, context
, event
)
555 self
.usequad(bm
, sel
[0], context
, event
)
557 elif checkforconnected('faces') in [1]:
558 if checkforconnected('edges') in [2]:
559 expand_vert(self
, context
, event
)
561 self
.usequad(bm
, sel
[0], context
, event
)
563 self
.usequad(bm
, sel
[0], context
, event
)
565 self
.usequad(bm
, sel
[0], context
, event
)
567 edges_sel
= [ed
for ed
in bm
.edges
if ed
.select
]
568 if len(edges_sel
) != 1:
569 # 2 vertices selected, but not on the same edge
570 bpy
.ops
.mesh
.edge_face_add()
572 # single edge selected -> new face from linked open edges
573 quad_from_edge(bm
, edges_sel
[0], context
, event
)
579 classes
= [MeshF2
, F2AddonPreferences
]
586 bpy
.utils
.register_class(c
)
589 kcfg
= bpy
.context
.window_manager
.keyconfigs
.addon
591 km
= kcfg
.keymaps
.new(name
='Mesh', space_type
='EMPTY')
592 kmi
= km
.keymap_items
.new("mesh.f2", 'F', 'PRESS')
593 addon_keymaps
.append((km
, kmi
.idname
))
597 # remove keymap entry
598 for km
, kmi_idname
in addon_keymaps
:
599 for kmi
in km
.keymap_items
:
600 if kmi
.idname
== kmi_idname
:
601 km
.keymap_items
.remove(kmi
)
602 addon_keymaps
.clear()
604 # remove operator and preferences
605 for c
in reversed(classes
):
606 bpy
.utils
.unregister_class(c
)
609 if __name__
== "__main__":