Import images: add file handler
[blender-addons.git] / mesh_f2.py
blob64b382f63e85a75d076f2c6fcbb33d29fc168062
1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Updated for 2.8 jan 5 2019
7 bl_info = {
8 "name": "F2",
9 "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
10 "(concept design), Adrian Rutkowski",
11 "version": (1, 8, 4),
12 "blender": (2, 80, 0),
13 "location": "Editmode > F",
14 "warning": "",
15 "description": "Extends the 'Make Edge/Face' functionality",
16 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/f2.html",
17 "category": "Mesh",
20 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
22 import bmesh
23 import bpy
24 import itertools
25 import mathutils
26 import math
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):
33 uv = None
34 uv_layer = None
35 if ob.material_slots:
36 me = ob.data
37 if me.uv_layers:
38 uv = me.uv_layers.active.name
39 # 'material_slots' is deprecated (Blender Internal)
40 # else:
41 # mat = ob.material_slots[mat_index].material
42 # if mat is not None:
43 # slot = mat.texture_slots[mat.active_texture_index]
44 # if slot and slot.uv_layer:
45 # uv = slot.uv_layer
46 # else:
47 # for tex_slot in mat.texture_slots:
48 # if tex_slot and tex_slot.uv_layer:
49 # uv = tex_slot.uv_layer
50 # break
51 if uv:
52 uv_layer = bm.loops.layers.uv.get(uv)
54 return (uv_layer)
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] \
69 for i in range(2)]
70 if not all_edges[0] or not all_edges[1]:
71 return
73 # determine which edges to use, based on mouse cursor position
74 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
75 optimal_edges = []
76 for edges in all_edges:
77 min_dist = False
78 for edge in 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,
82 region_3d, world_pos)
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]
96 # normal detection
97 flip_align = True
98 normal_edge = edge_1
99 if not normal_edge.link_faces:
100 normal_edge = edge_2
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
105 flip_align = False
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:
109 va_1 = v3
110 va_2 = v1
111 elif normal_edge == edge_sel:
112 va_1 = v1
113 va_2 = v2
114 else:
115 va_1 = v2
116 va_2 = v4
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
123 flip_align = False
125 # material index detection
126 ref_faces = edge_sel.link_faces
127 if not ref_faces:
128 ref_faces = edge_sel.verts[0].link_faces
129 if not ref_faces:
130 ref_faces = edge_sel.verts[1].link_faces
131 if not ref_faces:
132 mat_index = False
133 smooth = False
134 else:
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
141 # create quad
142 try:
143 if v3 == v4:
144 # triangle (usually at end of quad-strip
145 verts = [v3, v1, v2]
146 else:
147 # normal face creation
148 verts = [v3, v1, v2, v4]
149 if flip_align:
150 verts.reverse()
151 face = bm.faces.new(verts)
152 if mat_index:
153 face.material_index = mat_index
154 face.smooth = smooth
155 except:
156 # face already exists
157 return
159 # change selection
160 edge_sel.select = False
161 for vert in edge_sel.verts:
162 vert.select = False
163 for edge in face.edges:
164 if edge.index < 0:
165 edge.select = True
166 v3.select = True
167 v4.select = True
169 # adjust uv-map
170 if __name__ != '__main__':
171 if addon_prefs.adjustuv:
172 uv_layer = get_uv_layer(ob, bm, mat_index)
173 if uv_layer:
174 uv_ori = {}
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
193 me = ob.data
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]
199 if len(edges) < 2:
200 return
202 # determine which edges to use, based on mouse cursor position
203 min_dist = False
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 \
207 if not vert.select]
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,
213 world_pos)
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
219 edges = min_dist[1]
220 other_verts = min_dist[2]
221 new_pos = min_dist[3]
222 vert_new = bm.verts.new(new_pos)
224 # normal detection
225 flip_align = True
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
231 flip_align = False
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]
236 va_2 = vert_sel
237 else:
238 va_1 = vert_sel
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
246 flip_align = False
248 # material index detection
249 ref_faces = vert_sel.link_faces
250 if not ref_faces:
251 mat_index = False
252 smooth = False
253 else:
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]
262 if flip_align:
263 verts.reverse()
264 face = bm.faces.new(verts)
265 if mat_index:
266 face.material_index = mat_index
267 face.smooth = smooth
269 # change selection
270 vert_new.select = True
271 vert_sel.select = False
273 # adjust uv-map
274 if __name__ != '__main__':
275 if addon_prefs.adjustuv:
276 uv_layer = get_uv_layer(ob, bm, mat_index)
277 if uv_layer:
278 uv_others = {}
279 uv_sel = None
280 uv_new = None
281 # get original uv coordinates
282 for i in range(2):
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
286 break
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
293 break
294 if uv_sel:
295 uv_new = 2 * (mid_other - uv_sel) + uv_sel
297 # set uv coordinates for new loops
298 if uv_new:
299 for loop in face.loops:
300 if loop.vert.index == -1:
301 x, y = uv_new
302 elif loop.vert.index in uv_others:
303 x, y = uv_others[loop.vert.index]
304 else:
305 x, y = uv_sel
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
317 me = obj.data
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
323 for v in bm.verts:
324 if v.select:
325 v_active = v
327 try:
328 depth_location = v_active.co
329 except:
330 return {'CANCELLED'}
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)
336 c_verts = []
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
339 for edges in linked:
340 if len(edges.link_faces) < 2:
341 edges.select = True
342 for v in edges.verts:
343 if v is not v_active:
344 c_verts.append(v)
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))
360 bm.normal_update()
361 bm.verts.ensure_lookup_table()
363 # Deselect not needed point and create new face
364 if dist1 < dist2:
365 c_verts[1].select = False
366 lleft = c_verts[0].link_faces
368 else:
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:
379 if faces in lleft:
380 cface = faces
381 if len(faces.verts) == 3:
382 bm.normal_update()
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')
387 return {'FINISHED'}
389 lverts = cface.verts
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...
394 # from L to R
395 if dist1 < dist2:
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]))
410 else:
411 pass
412 # from R to L
413 else:
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))
428 else:
429 pass
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
434 else:
435 face_new.material_index = mat_index
436 face_new.smooth = smooth
438 # update normals
439 bpy.ops.mesh.select_all(action='DESELECT')
440 v_new.select = True
441 bm.select_history.add(v_new)
443 bm.normal_update()
444 bmesh.update_edit_mesh(obj.data)
445 bpy.ops.transform.translate('INVOKE_DEFAULT')
448 def checkforconnected(connection):
449 obj = bpy.context.object
450 me = obj.data
451 bm = bmesh.from_edit_mesh(me)
453 # Checks for number of edes or faces connected to selected vertex
454 for v in bm.verts:
455 if v.select:
456 v_active = v
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)
463 return len(linked)
466 # autograb preference in addons panel
467 class F2AddonPreferences(bpy.types.AddonPreferences):
468 bl_idname = __name__
469 adjustuv : bpy.props.BoolProperty(
470 name="Adjust UV",
471 description="Automatically update UV unwrapping",
472 default=False)
473 autograb : bpy.props.BoolProperty(
474 name="Auto Grab",
475 description="Automatically puts a newly created vertex in grab mode",
476 default=True)
477 extendvert : bpy.props.BoolProperty(
478 name="Enable Extend Vert",
479 description="Enables a way to build tris and quads by adding verts",
480 default=False)
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",
484 default=True)
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",
488 default=True)
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",
492 default=True)
493 ngons_v_mat : bpy.props.BoolProperty(
494 name="Ngons",
495 description="Use active material for created face instead of close one",
496 default=True)
498 def draw(self, context):
499 layout = self.layout
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):
516 """Tooltip"""
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'}
522 @classmethod
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]
538 if len(sel) > 2:
539 # original 'Make Edge/Face' behaviour
540 try:
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()
545 except:
546 return {'CANCELLED'}
547 elif len(sel) == 1:
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)
554 else:
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)
560 else:
561 self.usequad(bm, sel[0], context, event)
562 else:
563 self.usequad(bm, sel[0], context, event)
564 else:
565 self.usequad(bm, sel[0], context, event)
566 elif len(sel) == 2:
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()
571 else:
572 # single edge selected -> new face from linked open edges
573 quad_from_edge(bm, edges_sel[0], context, event)
575 return {'FINISHED'}
578 # registration
579 classes = [MeshF2, F2AddonPreferences]
580 addon_keymaps = []
583 def register():
584 # add operator
585 for c in classes:
586 bpy.utils.register_class(c)
588 # add keymap entry
589 kcfg = bpy.context.window_manager.keyconfigs.addon
590 if kcfg:
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))
596 def unregister():
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__":
610 register()