1 # Updated for 2.8 jan 5 2019
3 # ##### BEGIN GPL LICENSE BLOCK #####
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.
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.
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.
19 # ##### END GPL LICENSE BLOCK #####
25 "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
26 "(concept design), Adrian Rutkowski",
28 "blender": (2, 80, 0),
29 "location": "Editmode > F",
31 "description": "Extends the 'Make Edge/Face' functionality",
32 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
33 "Scripts/Modeling/F2",
37 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
44 from mathutils
import Vector
45 from bpy_extras
import view3d_utils
48 # returns a custom data layer of the UV map, or None
49 def get_uv_layer(ob
, bm
, mat_index
):
55 uv
= me
.uv_layers
.active
.name
56 # 'material_slots' is deprecated (Blender Internal)
58 # mat = ob.material_slots[mat_index].material
60 # slot = mat.texture_slots[mat.active_texture_index]
61 # if slot and slot.uv_layer:
64 # for tex_slot in mat.texture_slots:
65 # if tex_slot and tex_slot.uv_layer:
66 # uv = tex_slot.uv_layer
69 uv_layer
= bm
.loops
.layers
.uv
.get(uv
)
74 # create a face from a single selected edge
75 def quad_from_edge(bm
, edge_sel
, context
, event
):
76 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
77 ob
= context
.active_object
78 region
= context
.region
79 region_3d
= context
.space_data
.region_3d
81 # find linked edges that are open (<2 faces connected) and not part of
82 # the face the selected edge belongs to
83 all_edges
= [[edge
for edge
in edge_sel
.verts
[i
].link_edges
if \
84 len(edge
.link_faces
) < 2 and edge
!= edge_sel
and \
85 sum([face
in edge_sel
.link_faces
for face
in edge
.link_faces
]) == 0] \
87 if not all_edges
[0] or not all_edges
[1]:
90 # determine which edges to use, based on mouse cursor position
91 mouse_pos
= mathutils
.Vector([event
.mouse_region_x
, event
.mouse_region_y
])
93 for edges
in all_edges
:
96 vert
= [vert
for vert
in edge
.verts
if not vert
.select
][0]
97 world_pos
= ob
.matrix_world
@ vert
.co
.copy()
98 screen_pos
= view3d_utils
.location_3d_to_region_2d(region
,
100 dist
= (mouse_pos
- screen_pos
).length
101 if not min_dist
or dist
< min_dist
[0]:
102 min_dist
= (dist
, edge
, vert
)
103 optimal_edges
.append(min_dist
)
105 # determine the vertices, which make up the quad
106 v1
= edge_sel
.verts
[0]
107 v2
= edge_sel
.verts
[1]
108 edge_1
= optimal_edges
[0][1]
109 edge_2
= optimal_edges
[1][1]
110 v3
= optimal_edges
[0][2]
111 v4
= optimal_edges
[1][2]
116 if not normal_edge
.link_faces
:
118 if not normal_edge
.link_faces
:
119 normal_edge
= edge_sel
120 if not normal_edge
.link_faces
:
121 # no connected faces, so no need to flip the face normal
123 if flip_align
: # there is a face to which the normal can be aligned
124 ref_verts
= [v
for v
in normal_edge
.link_faces
[0].verts
]
125 if v3
in ref_verts
and v1
in ref_verts
:
128 elif normal_edge
== edge_sel
:
134 if (va_1
== ref_verts
[0] and va_2
== ref_verts
[-1]) or \
135 (va_2
== ref_verts
[0] and va_1
== ref_verts
[-1]):
136 # reference verts are at start and end of the list -> shift list
137 ref_verts
= ref_verts
[1:] + [ref_verts
[0]]
138 if ref_verts
.index(va_1
) > ref_verts
.index(va_2
):
139 # connected face has same normal direction, so don't flip
142 # material index detection
143 ref_faces
= edge_sel
.link_faces
145 ref_faces
= edge_sel
.verts
[0].link_faces
147 ref_faces
= edge_sel
.verts
[1].link_faces
152 mat_index
= ref_faces
[0].material_index
153 smooth
= ref_faces
[0].smooth
155 if addon_prefs
.quad_from_e_mat
:
156 mat_index
= bpy
.context
.object.active_material_index
161 # triangle (usually at end of quad-strip
164 # normal face creation
165 verts
= [v3
, v1
, v2
, v4
]
168 face
= bm
.faces
.new(verts
)
170 face
.material_index
= mat_index
173 # face already exists
177 edge_sel
.select
= False
178 for vert
in edge_sel
.verts
:
180 for edge
in face
.edges
:
187 if __name__
!= '__main__':
188 if addon_prefs
.adjustuv
:
189 uv_layer
= get_uv_layer(ob
, bm
, mat_index
)
192 for vert
in [v1
, v2
, v3
, v4
]:
193 for loop
in vert
.link_loops
:
194 if loop
.face
.index
> -1:
195 uv_ori
[loop
.vert
.index
] = loop
[uv_layer
].uv
196 if len(uv_ori
) == 4 or len(uv_ori
) == 3:
197 for loop
in face
.loops
:
198 if loop
.vert
.index
in uv_ori
:
199 loop
[uv_layer
].uv
= uv_ori
[loop
.vert
.index
]
201 # toggle mode, to force correct drawing
202 bpy
.ops
.object.mode_set(mode
='OBJECT')
203 bpy
.ops
.object.mode_set(mode
='EDIT')
206 # create a face from a single selected vertex, if it is an open vertex
207 def quad_from_vertex(bm
, vert_sel
, context
, event
):
208 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
209 ob
= context
.active_object
211 region
= context
.region
212 region_3d
= context
.space_data
.region_3d
214 # find linked edges that are open (<2 faces connected)
215 edges
= [edge
for edge
in vert_sel
.link_edges
if len(edge
.link_faces
) < 2]
219 # determine which edges to use, based on mouse cursor position
221 mouse_pos
= mathutils
.Vector([event
.mouse_region_x
, event
.mouse_region_y
])
222 for a
, b
in itertools
.combinations(edges
, 2):
223 other_verts
= [vert
for edge
in [a
, b
] for vert
in edge
.verts \
225 mid_other
= (other_verts
[0].co
.copy() + other_verts
[1].co
.copy()) \
227 new_pos
= 2 * (mid_other
- vert_sel
.co
.copy()) + vert_sel
.co
.copy()
228 world_pos
= ob
.matrix_world
@ new_pos
229 screen_pos
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
231 dist
= (mouse_pos
- screen_pos
).length
232 if not min_dist
or dist
< min_dist
[0]:
233 min_dist
= (dist
, (a
, b
), other_verts
, new_pos
)
235 # create vertex at location mirrored in the line, connecting the open edges
237 other_verts
= min_dist
[2]
238 new_pos
= min_dist
[3]
239 vert_new
= bm
.verts
.new(new_pos
)
243 normal_edge
= edges
[0]
244 if not normal_edge
.link_faces
:
245 normal_edge
= edges
[1]
246 if not normal_edge
.link_faces
:
247 # no connected faces, so no need to flip the face normal
249 if flip_align
: # there is a face to which the normal can be aligned
250 ref_verts
= [v
for v
in normal_edge
.link_faces
[0].verts
]
251 if other_verts
[0] in ref_verts
:
252 va_1
= other_verts
[0]
256 va_2
= other_verts
[1]
257 if (va_1
== ref_verts
[0] and va_2
== ref_verts
[-1]) or \
258 (va_2
== ref_verts
[0] and va_1
== ref_verts
[-1]):
259 # reference verts are at start and end of the list -> shift list
260 ref_verts
= ref_verts
[1:] + [ref_verts
[0]]
261 if ref_verts
.index(va_1
) > ref_verts
.index(va_2
):
262 # connected face has same normal direction, so don't flip
265 # material index detection
266 ref_faces
= vert_sel
.link_faces
271 mat_index
= ref_faces
[0].material_index
272 smooth
= ref_faces
[0].smooth
274 if addon_prefs
.quad_from_v_mat
:
275 mat_index
= bpy
.context
.object.active_material_index
277 # create face between all 4 vertices involved
278 verts
= [other_verts
[0], vert_sel
, other_verts
[1], vert_new
]
281 face
= bm
.faces
.new(verts
)
283 face
.material_index
= mat_index
287 vert_new
.select
= True
288 vert_sel
.select
= False
291 if __name__
!= '__main__':
292 if addon_prefs
.adjustuv
:
293 uv_layer
= get_uv_layer(ob
, bm
, mat_index
)
298 # get original uv coordinates
300 for loop
in other_verts
[i
].link_loops
:
301 if loop
.face
.index
> -1:
302 uv_others
[loop
.vert
.index
] = loop
[uv_layer
].uv
304 if len(uv_others
) == 2:
305 mid_other
= (list(uv_others
.values())[0] +
306 list(uv_others
.values())[1]) / 2
307 for loop
in vert_sel
.link_loops
:
308 if loop
.face
.index
> -1:
309 uv_sel
= loop
[uv_layer
].uv
312 uv_new
= 2 * (mid_other
- uv_sel
) + uv_sel
314 # set uv coordinates for new loops
316 for loop
in face
.loops
:
317 if loop
.vert
.index
== -1:
319 elif loop
.vert
.index
in uv_others
:
320 x
, y
= uv_others
[loop
.vert
.index
]
323 loop
[uv_layer
].uv
= (x
, y
)
325 # toggle mode, to force correct drawing
326 bpy
.ops
.object.mode_set(mode
='OBJECT')
327 bpy
.ops
.object.mode_set(mode
='EDIT')
330 def expand_vert(self
, context
, event
):
331 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
332 ob
= context
.active_object
333 obj
= bpy
.context
.object
335 bm
= bmesh
.from_edit_mesh(me
)
336 region
= context
.region
337 region_3d
= context
.space_data
.region_3d
338 rv3d
= context
.space_data
.region_3d
345 depth_location
= v_active
.co
348 # create vert in mouse cursor location
350 mouse_pos
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
351 location_3d
= view3d_utils
.region_2d_to_location_3d(region
, rv3d
, mouse_pos
, depth_location
)
354 # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
355 linked
= v_active
.link_edges
357 if len(edges
.link_faces
) < 2:
359 for v
in edges
.verts
:
360 if v
is not v_active
:
363 # Compare distance in 2d between mouse and edges middle points
364 screen_pos_va
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
365 ob
.matrix_world
@ v_active
.co
)
366 screen_pos_v1
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
367 ob
.matrix_world
@ c_verts
[0].co
)
368 screen_pos_v2
= view3d_utils
.location_3d_to_region_2d(region
, region_3d
,
369 ob
.matrix_world
@ c_verts
[1].co
)
371 mid_pos_v1
= Vector(((screen_pos_va
[0] + screen_pos_v1
[0]) / 2, (screen_pos_va
[1] + screen_pos_v1
[1]) / 2))
372 mid_pos_V2
= Vector(((screen_pos_va
[0] + screen_pos_v2
[0]) / 2, (screen_pos_va
[1] + screen_pos_v2
[1]) / 2))
374 dist1
= math
.log10(pow((mid_pos_v1
[0] - mouse_pos
[0]), 2) + pow((mid_pos_v1
[1] - mouse_pos
[1]), 2))
375 dist2
= math
.log10(pow((mid_pos_V2
[0] - mouse_pos
[0]), 2) + pow((mid_pos_V2
[1] - mouse_pos
[1]), 2))
378 bm
.verts
.ensure_lookup_table()
380 # Deselect not needed point and create new face
382 c_verts
[1].select
= False
383 lleft
= c_verts
[0].link_faces
386 c_verts
[0].select
= False
387 lleft
= c_verts
[1].link_faces
389 lactive
= v_active
.link_faces
390 # lverts = lactive[0].verts
392 mat_index
= lactive
[0].material_index
393 smooth
= lactive
[0].smooth
395 for faces
in lactive
:
398 if len(faces
.verts
) == 3:
400 bmesh
.update_edit_mesh(obj
.data
)
401 bpy
.ops
.mesh
.select_all(action
='DESELECT')
402 v_active
.select
= True
403 bpy
.ops
.mesh
.rip_edge_move('INVOKE_DEFAULT')
408 # create triangle with correct normal orientation
409 # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...
413 if (lverts
[0] == v_active
and lverts
[3] == c_verts
[0]) \
414 or (lverts
[2] == v_active
and lverts
[1] == c_verts
[0]) \
415 or (lverts
[1] == v_active
and lverts
[0] == c_verts
[0]) \
416 or (lverts
[3] == v_active
and lverts
[2] == c_verts
[0]):
417 v_new
= bm
.verts
.new(v_active
.co
)
418 face_new
= bm
.faces
.new((c_verts
[0], v_new
, v_active
))
420 elif (lverts
[1] == v_active
and lverts
[2] == c_verts
[0]) \
421 or (lverts
[0] == v_active
and lverts
[1] == c_verts
[0]) \
422 or (lverts
[3] == v_active
and lverts
[0] == c_verts
[0]) \
423 or (lverts
[2] == v_active
and lverts
[3] == c_verts
[0]):
424 v_new
= bm
.verts
.new(v_active
.co
)
425 face_new
= bm
.faces
.new((v_active
, v_new
, c_verts
[0]))
431 if (lverts
[2] == v_active
and lverts
[3] == c_verts
[1]) \
432 or (lverts
[0] == v_active
and lverts
[1] == c_verts
[1]) \
433 or (lverts
[1] == v_active
and lverts
[2] == c_verts
[1]) \
434 or (lverts
[3] == v_active
and lverts
[0] == c_verts
[1]):
435 v_new
= bm
.verts
.new(v_active
.co
)
436 face_new
= bm
.faces
.new((v_active
, v_new
, c_verts
[1]))
438 elif (lverts
[0] == v_active
and lverts
[3] == c_verts
[1]) \
439 or (lverts
[2] == v_active
and lverts
[1] == c_verts
[1]) \
440 or (lverts
[1] == v_active
and lverts
[0] == c_verts
[1]) \
441 or (lverts
[3] == v_active
and lverts
[2] == c_verts
[1]):
442 v_new
= bm
.verts
.new(v_active
.co
)
443 face_new
= bm
.faces
.new((c_verts
[1], v_new
, v_active
))
448 # set smooth and mat based on starting face
449 if addon_prefs
.tris_from_v_mat
:
450 face_new
.material_index
= bpy
.context
.object.active_material_index
452 face_new
.material_index
= mat_index
453 face_new
.smooth
= smooth
456 bpy
.ops
.mesh
.select_all(action
='DESELECT')
458 bm
.select_history
.add(v_new
)
461 bmesh
.update_edit_mesh(obj
.data
)
462 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
465 def checkforconnected(conection
):
466 obj
= bpy
.context
.object
468 bm
= bmesh
.from_edit_mesh(me
)
470 # Checks for number of edes or faces connected to selected vertex
474 if conection
== 'faces':
475 linked
= v_active
.link_faces
476 elif conection
== 'edges':
477 linked
= v_active
.link_edges
479 bmesh
.update_edit_mesh(obj
.data
)
483 # autograb preference in addons panel
484 class F2AddonPreferences(bpy
.types
.AddonPreferences
):
486 adjustuv
: bpy
.props
.BoolProperty(
488 description
="Automatically update UV unwrapping",
490 autograb
: bpy
.props
.BoolProperty(
492 description
="Automatically puts a newly created vertex in grab mode",
494 extendvert
: bpy
.props
.BoolProperty(
495 name
="Enable Extend Vert",
496 description
="Anables a way to build tris and quads by adding verts",
498 quad_from_e_mat
: bpy
.props
.BoolProperty(
499 name
="Quad From Edge",
500 description
="Use active material for created face instead of close one",
502 quad_from_v_mat
: bpy
.props
.BoolProperty(
503 name
="Quad From Vert",
504 description
="Use active material for created face instead of close one",
506 tris_from_v_mat
: bpy
.props
.BoolProperty(
507 name
="Tris From Vert",
508 description
="Use active material for created face instead of close one",
510 ngons_v_mat
: bpy
.props
.BoolProperty(
512 description
="Use active material for created face instead of close one",
515 def draw(self
, context
):
518 col
= layout
.column()
519 col
.label(text
="behaviours:")
520 col
.prop(self
, "autograb")
521 col
.prop(self
, "adjustuv")
522 col
.prop(self
, "extendvert")
524 col
= layout
.column()
525 col
.label(text
="use active material when creating:")
526 col
.prop(self
, "quad_from_e_mat")
527 col
.prop(self
, "quad_from_v_mat")
528 col
.prop(self
, "tris_from_v_mat")
529 col
.prop(self
, "ngons_v_mat")
532 class MeshF2(bpy
.types
.Operator
):
534 bl_idname
= "mesh.f2"
535 bl_label
= "Make Edge/Face"
536 bl_description
= "Extends the 'Make Edge/Face' functionality"
537 bl_options
= {'REGISTER', 'UNDO'}
540 def poll(cls
, context
):
541 # check we are in mesh editmode
542 ob
= context
.active_object
543 return (ob
and ob
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
545 def usequad(self
, bm
, sel
, context
, event
):
546 quad_from_vertex(bm
, sel
, context
, event
)
547 if __name__
!= '__main__':
548 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
549 if addon_prefs
.autograb
:
550 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
552 def invoke(self
, context
, event
):
553 bm
= bmesh
.from_edit_mesh(context
.active_object
.data
)
554 sel
= [v
for v
in bm
.verts
if v
.select
]
556 # original 'Make Edge/Face' behaviour
558 bpy
.ops
.mesh
.edge_face_add('INVOKE_DEFAULT')
559 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
560 if addon_prefs
.ngons_v_mat
:
561 bpy
.ops
.object.material_slot_assign()
565 # single vertex selected -> mirror vertex and create new face
566 addon_prefs
= context
.preferences
.addons
[__name__
].preferences
567 if addon_prefs
.extendvert
:
568 if checkforconnected('faces') in [2]:
569 if checkforconnected('edges') in [3]:
570 expand_vert(self
, context
, event
)
572 self
.usequad(bm
, sel
[0], context
, event
)
574 elif checkforconnected('faces') in [1]:
575 if checkforconnected('edges') in [2]:
576 expand_vert(self
, context
, event
)
578 self
.usequad(bm
, sel
[0], context
, event
)
580 self
.usequad(bm
, sel
[0], context
, event
)
582 self
.usequad(bm
, sel
[0], context
, event
)
584 edges_sel
= [ed
for ed
in bm
.edges
if ed
.select
]
585 if len(edges_sel
) != 1:
586 # 2 vertices selected, but not on the same edge
587 bpy
.ops
.mesh
.edge_face_add()
589 # single edge selected -> new face from linked open edges
590 quad_from_edge(bm
, edges_sel
[0], context
, event
)
596 classes
= [MeshF2
, F2AddonPreferences
]
603 bpy
.utils
.register_class(c
)
606 kcfg
= bpy
.context
.window_manager
.keyconfigs
.addon
608 km
= kcfg
.keymaps
.new(name
='Mesh', space_type
='EMPTY')
609 kmi
= km
.keymap_items
.new("mesh.f2", 'F', 'PRESS')
610 addon_keymaps
.append((km
, kmi
.idname
))
614 # remove keymap entry
615 for km
, kmi_idname
in addon_keymaps
:
616 for kmi
in km
.keymap_items
:
617 if kmi
.idname
== kmi_idname
:
618 km
.keymap_items
.remove(kmi
)
619 addon_keymaps
.clear()
621 # remove operator and preferences
622 for c
in reversed(classes
):
623 bpy
.utils
.unregister_class(c
)
626 if __name__
== "__main__":