Rigify: store advanced options in armature instead of window manager.
[blender-addons.git] / mesh_f2.py
blobc6dc74a62da9ca6cc5045d9cf3d0db6e2432cebd
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 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
33 "Scripts/Modeling/F2",
34 "category": "Mesh",
37 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
39 import bmesh
40 import bpy
41 import itertools
42 import mathutils
43 import math
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):
50 uv = None
51 uv_layer = None
52 if ob.material_slots:
53 me = ob.data
54 if me.uv_layers:
55 uv = me.uv_layers.active.name
56 # 'material_slots' is deprecated (Blender Internal)
57 # else:
58 # mat = ob.material_slots[mat_index].material
59 # if mat is not None:
60 # slot = mat.texture_slots[mat.active_texture_index]
61 # if slot and slot.uv_layer:
62 # uv = slot.uv_layer
63 # else:
64 # for tex_slot in mat.texture_slots:
65 # if tex_slot and tex_slot.uv_layer:
66 # uv = tex_slot.uv_layer
67 # break
68 if uv:
69 uv_layer = bm.loops.layers.uv.get(uv)
71 return (uv_layer)
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] \
86 for i in range(2)]
87 if not all_edges[0] or not all_edges[1]:
88 return
90 # determine which edges to use, based on mouse cursor position
91 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
92 optimal_edges = []
93 for edges in all_edges:
94 min_dist = False
95 for edge in 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,
99 region_3d, world_pos)
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]
113 # normal detection
114 flip_align = True
115 normal_edge = edge_1
116 if not normal_edge.link_faces:
117 normal_edge = edge_2
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
122 flip_align = False
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:
126 va_1 = v3
127 va_2 = v1
128 elif normal_edge == edge_sel:
129 va_1 = v1
130 va_2 = v2
131 else:
132 va_1 = v2
133 va_2 = v4
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
140 flip_align = False
142 # material index detection
143 ref_faces = edge_sel.link_faces
144 if not ref_faces:
145 ref_faces = edge_sel.verts[0].link_faces
146 if not ref_faces:
147 ref_faces = edge_sel.verts[1].link_faces
148 if not ref_faces:
149 mat_index = False
150 smooth = False
151 else:
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
158 # create quad
159 try:
160 if v3 == v4:
161 # triangle (usually at end of quad-strip
162 verts = [v3, v1, v2]
163 else:
164 # normal face creation
165 verts = [v3, v1, v2, v4]
166 if flip_align:
167 verts.reverse()
168 face = bm.faces.new(verts)
169 if mat_index:
170 face.material_index = mat_index
171 face.smooth = smooth
172 except:
173 # face already exists
174 return
176 # change selection
177 edge_sel.select = False
178 for vert in edge_sel.verts:
179 vert.select = False
180 for edge in face.edges:
181 if edge.index < 0:
182 edge.select = True
183 v3.select = True
184 v4.select = True
186 # adjust uv-map
187 if __name__ != '__main__':
188 if addon_prefs.adjustuv:
189 uv_layer = get_uv_layer(ob, bm, mat_index)
190 if uv_layer:
191 uv_ori = {}
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
210 me = ob.data
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]
216 if len(edges) < 2:
217 return
219 # determine which edges to use, based on mouse cursor position
220 min_dist = False
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 \
224 if not vert.select]
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,
230 world_pos)
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
236 edges = min_dist[1]
237 other_verts = min_dist[2]
238 new_pos = min_dist[3]
239 vert_new = bm.verts.new(new_pos)
241 # normal detection
242 flip_align = True
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
248 flip_align = False
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]
253 va_2 = vert_sel
254 else:
255 va_1 = vert_sel
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
263 flip_align = False
265 # material index detection
266 ref_faces = vert_sel.link_faces
267 if not ref_faces:
268 mat_index = False
269 smooth = False
270 else:
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]
279 if flip_align:
280 verts.reverse()
281 face = bm.faces.new(verts)
282 if mat_index:
283 face.material_index = mat_index
284 face.smooth = smooth
286 # change selection
287 vert_new.select = True
288 vert_sel.select = False
290 # adjust uv-map
291 if __name__ != '__main__':
292 if addon_prefs.adjustuv:
293 uv_layer = get_uv_layer(ob, bm, mat_index)
294 if uv_layer:
295 uv_others = {}
296 uv_sel = None
297 uv_new = None
298 # get original uv coordinates
299 for i in range(2):
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
303 break
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
310 break
311 if uv_sel:
312 uv_new = 2 * (mid_other - uv_sel) + uv_sel
314 # set uv coordinates for new loops
315 if uv_new:
316 for loop in face.loops:
317 if loop.vert.index == -1:
318 x, y = uv_new
319 elif loop.vert.index in uv_others:
320 x, y = uv_others[loop.vert.index]
321 else:
322 x, y = uv_sel
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
334 me = obj.data
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
340 for v in bm.verts:
341 if v.select:
342 v_active = v
344 try:
345 depth_location = v_active.co
346 except:
347 return {'CANCELLED'}
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)
353 c_verts = []
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
356 for edges in linked:
357 if len(edges.link_faces) < 2:
358 edges.select = True
359 for v in edges.verts:
360 if v is not v_active:
361 c_verts.append(v)
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))
377 bm.normal_update()
378 bm.verts.ensure_lookup_table()
380 # Deselect not needed point and create new face
381 if dist1 < dist2:
382 c_verts[1].select = False
383 lleft = c_verts[0].link_faces
385 else:
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:
396 if faces in lleft:
397 cface = faces
398 if len(faces.verts) == 3:
399 bm.normal_update()
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')
404 return {'FINISHED'}
406 lverts = cface.verts
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...
411 # from L to R
412 if dist1 < dist2:
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]))
427 else:
428 pass
429 # from R to L
430 else:
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))
445 else:
446 pass
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
451 else:
452 face_new.material_index = mat_index
453 face_new.smooth = smooth
455 # update normals
456 bpy.ops.mesh.select_all(action='DESELECT')
457 v_new.select = True
458 bm.select_history.add(v_new)
460 bm.normal_update()
461 bmesh.update_edit_mesh(obj.data)
462 bpy.ops.transform.translate('INVOKE_DEFAULT')
465 def checkforconnected(conection):
466 obj = bpy.context.object
467 me = obj.data
468 bm = bmesh.from_edit_mesh(me)
470 # Checks for number of edes or faces connected to selected vertex
471 for v in bm.verts:
472 if v.select:
473 v_active = v
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)
480 return len(linked)
483 # autograb preference in addons panel
484 class F2AddonPreferences(bpy.types.AddonPreferences):
485 bl_idname = __name__
486 adjustuv : bpy.props.BoolProperty(
487 name="Adjust UV",
488 description="Automatically update UV unwrapping",
489 default=False)
490 autograb : bpy.props.BoolProperty(
491 name="Auto Grab",
492 description="Automatically puts a newly created vertex in grab mode",
493 default=True)
494 extendvert : bpy.props.BoolProperty(
495 name="Enable Extend Vert",
496 description="Anables a way to build tris and quads by adding verts",
497 default=False)
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",
501 default=True)
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",
505 default=True)
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",
509 default=True)
510 ngons_v_mat : bpy.props.BoolProperty(
511 name="Ngons",
512 description="Use active material for created face instead of close one",
513 default=True)
515 def draw(self, context):
516 layout = self.layout
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):
533 """Tooltip"""
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'}
539 @classmethod
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]
555 if len(sel) > 2:
556 # original 'Make Edge/Face' behaviour
557 try:
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()
562 except:
563 return {'CANCELLED'}
564 elif len(sel) == 1:
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)
571 else:
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)
577 else:
578 self.usequad(bm, sel[0], context, event)
579 else:
580 self.usequad(bm, sel[0], context, event)
581 else:
582 self.usequad(bm, sel[0], context, event)
583 elif len(sel) == 2:
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()
588 else:
589 # single edge selected -> new face from linked open edges
590 quad_from_edge(bm, edges_sel[0], context, event)
592 return {'FINISHED'}
595 # registration
596 classes = [MeshF2, F2AddonPreferences]
597 addon_keymaps = []
600 def register():
601 # add operator
602 for c in classes:
603 bpy.utils.register_class(c)
605 # add keymap entry
606 kcfg = bpy.context.window_manager.keyconfigs.addon
607 if kcfg:
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))
613 def unregister():
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__":
627 register()