Cleanup: trailing space
[blender-addons.git] / mesh_f2.py
blob569b86de9023493590c525a8db613252c82d15a6
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 #####
21 # <pep8 compliant>
23 bl_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",
36 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
38 import bmesh
39 import bpy
40 import itertools
41 import mathutils
42 import math
43 from mathutils import Vector
44 from bpy_extras import view3d_utils
47 # returns a custom data layer of the UV map, or None
48 def 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)
70 return (uv_layer)
73 # create a face from a single selected edge
74 def 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
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
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)
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]
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
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
154 if addon_prefs.quad_from_e_mat:
155 mat_index = bpy.context.object.active_material_index
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
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
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]
200 # toggle mode, to force correct drawing
201 bpy.ops.object.mode_set(mode='OBJECT')
202 bpy.ops.object.mode_set(mode='EDIT')
205 # create a face from a single selected vertex, if it is an open vertex
206 def 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
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
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()) \
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)
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)
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
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
273 if addon_prefs.quad_from_v_mat:
274 mat_index = bpy.context.object.active_material_index
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
285 # change selection
286 vert_new.select = True
287 vert_sel.select = False
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
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)
324 # toggle mode, to force correct drawing
325 bpy.ops.object.mode_set(mode='OBJECT')
326 bpy.ops.object.mode_set(mode='EDIT')
329 def 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
339 for v in bm.verts:
340 if v.select:
341 v_active = v
343 try:
344 depth_location = v_active.co
345 except:
346 return {'CANCELLED'}
347 # create vert in mouse cursor location
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)
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)
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)
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))
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))
376 bm.normal_update()
377 bm.verts.ensure_lookup_table()
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
384 else:
385 c_verts[0].select = False
386 lleft = c_verts[1].link_faces
388 lactive = v_active.link_faces
389 # lverts = lactive[0].verts
391 mat_index = lactive[0].material_index
392 smooth = lactive[0].smooth
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'}
405 lverts = cface.verts
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...
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))
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]))
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]))
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))
444 else:
445 pass
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
454 # update normals
455 bpy.ops.mesh.select_all(action='DESELECT')
456 v_new.select = True
457 bm.select_history.add(v_new)
459 bm.normal_update()
460 bmesh.update_edit_mesh(obj.data)
461 bpy.ops.transform.translate('INVOKE_DEFAULT')
464 def checkforconnected(conection):
465 obj = bpy.context.object
466 me = obj.data
467 bm = bmesh.from_edit_mesh(me)
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
478 bmesh.update_edit_mesh(obj.data)
479 return len(linked)
482 # autograb preference in addons panel
483 class 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)
514 def draw(self, context):
515 layout = self.layout
517 col = layout.column()
518 col.label(text="behaviours:")
519 col.prop(self, "autograb")
520 col.prop(self, "adjustuv")
521 col.prop(self, "extendvert")
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")
531 class 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'}
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')
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')
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)
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)
591 return {'FINISHED'}
594 # registration
595 classes = [MeshF2, F2AddonPreferences]
596 addon_keymaps = []
599 def register():
600 # add operator
601 for c in classes:
602 bpy.utils.register_class(c)
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))
612 def 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()
620 # remove operator and preferences
621 for c in reversed(classes):
622 bpy.utils.unregister_class(c)
625 if __name__ == "__main__":
626 register()