io_mesh_uv_layout: speed up png export with OIIO (x7)
[blender-addons.git] / mesh_f2.py
blob2bfcceb201e5b471ac792c05d8e2f66ab2643596
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Updated for 2.8 jan 5 2019
5 bl_info = {
6 "name": "F2",
7 "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
8 "(concept design), Adrian Rutkowski",
9 "version": (1, 8, 4),
10 "blender": (2, 80, 0),
11 "location": "Editmode > F",
12 "warning": "",
13 "description": "Extends the 'Make Edge/Face' functionality",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/f2.html",
15 "category": "Mesh",
18 # ref: https://github.com/Cfyzzz/Other-scripts/blob/master/f2.py
20 import bmesh
21 import bpy
22 import itertools
23 import mathutils
24 import math
25 from mathutils import Vector
26 from bpy_extras import view3d_utils
29 # returns a custom data layer of the UV map, or None
30 def get_uv_layer(ob, bm, mat_index):
31 uv = None
32 uv_layer = None
33 if ob.material_slots:
34 me = ob.data
35 if me.uv_layers:
36 uv = me.uv_layers.active.name
37 # 'material_slots' is deprecated (Blender Internal)
38 # else:
39 # mat = ob.material_slots[mat_index].material
40 # if mat is not None:
41 # slot = mat.texture_slots[mat.active_texture_index]
42 # if slot and slot.uv_layer:
43 # uv = slot.uv_layer
44 # else:
45 # for tex_slot in mat.texture_slots:
46 # if tex_slot and tex_slot.uv_layer:
47 # uv = tex_slot.uv_layer
48 # break
49 if uv:
50 uv_layer = bm.loops.layers.uv.get(uv)
52 return (uv_layer)
55 # create a face from a single selected edge
56 def quad_from_edge(bm, edge_sel, context, event):
57 addon_prefs = context.preferences.addons[__name__].preferences
58 ob = context.active_object
59 region = context.region
60 region_3d = context.space_data.region_3d
62 # find linked edges that are open (<2 faces connected) and not part of
63 # the face the selected edge belongs to
64 all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
65 len(edge.link_faces) < 2 and edge != edge_sel and \
66 sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
67 for i in range(2)]
68 if not all_edges[0] or not all_edges[1]:
69 return
71 # determine which edges to use, based on mouse cursor position
72 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
73 optimal_edges = []
74 for edges in all_edges:
75 min_dist = False
76 for edge in edges:
77 vert = [vert for vert in edge.verts if not vert.select][0]
78 world_pos = ob.matrix_world @ vert.co.copy()
79 screen_pos = view3d_utils.location_3d_to_region_2d(region,
80 region_3d, world_pos)
81 dist = (mouse_pos - screen_pos).length
82 if not min_dist or dist < min_dist[0]:
83 min_dist = (dist, edge, vert)
84 optimal_edges.append(min_dist)
86 # determine the vertices, which make up the quad
87 v1 = edge_sel.verts[0]
88 v2 = edge_sel.verts[1]
89 edge_1 = optimal_edges[0][1]
90 edge_2 = optimal_edges[1][1]
91 v3 = optimal_edges[0][2]
92 v4 = optimal_edges[1][2]
94 # normal detection
95 flip_align = True
96 normal_edge = edge_1
97 if not normal_edge.link_faces:
98 normal_edge = edge_2
99 if not normal_edge.link_faces:
100 normal_edge = edge_sel
101 if not normal_edge.link_faces:
102 # no connected faces, so no need to flip the face normal
103 flip_align = False
104 if flip_align: # there is a face to which the normal can be aligned
105 ref_verts = [v for v in normal_edge.link_faces[0].verts]
106 if v3 in ref_verts and v1 in ref_verts:
107 va_1 = v3
108 va_2 = v1
109 elif normal_edge == edge_sel:
110 va_1 = v1
111 va_2 = v2
112 else:
113 va_1 = v2
114 va_2 = v4
115 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
116 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
117 # reference verts are at start and end of the list -> shift list
118 ref_verts = ref_verts[1:] + [ref_verts[0]]
119 if ref_verts.index(va_1) > ref_verts.index(va_2):
120 # connected face has same normal direction, so don't flip
121 flip_align = False
123 # material index detection
124 ref_faces = edge_sel.link_faces
125 if not ref_faces:
126 ref_faces = edge_sel.verts[0].link_faces
127 if not ref_faces:
128 ref_faces = edge_sel.verts[1].link_faces
129 if not ref_faces:
130 mat_index = False
131 smooth = False
132 else:
133 mat_index = ref_faces[0].material_index
134 smooth = ref_faces[0].smooth
136 if addon_prefs.quad_from_e_mat:
137 mat_index = bpy.context.object.active_material_index
139 # create quad
140 try:
141 if v3 == v4:
142 # triangle (usually at end of quad-strip
143 verts = [v3, v1, v2]
144 else:
145 # normal face creation
146 verts = [v3, v1, v2, v4]
147 if flip_align:
148 verts.reverse()
149 face = bm.faces.new(verts)
150 if mat_index:
151 face.material_index = mat_index
152 face.smooth = smooth
153 except:
154 # face already exists
155 return
157 # change selection
158 edge_sel.select = False
159 for vert in edge_sel.verts:
160 vert.select = False
161 for edge in face.edges:
162 if edge.index < 0:
163 edge.select = True
164 v3.select = True
165 v4.select = True
167 # adjust uv-map
168 if __name__ != '__main__':
169 if addon_prefs.adjustuv:
170 uv_layer = get_uv_layer(ob, bm, mat_index)
171 if uv_layer:
172 uv_ori = {}
173 for vert in [v1, v2, v3, v4]:
174 for loop in vert.link_loops:
175 if loop.face.index > -1:
176 uv_ori[loop.vert.index] = loop[uv_layer].uv
177 if len(uv_ori) == 4 or len(uv_ori) == 3:
178 for loop in face.loops:
179 if loop.vert.index in uv_ori:
180 loop[uv_layer].uv = uv_ori[loop.vert.index]
182 # toggle mode, to force correct drawing
183 bpy.ops.object.mode_set(mode='OBJECT')
184 bpy.ops.object.mode_set(mode='EDIT')
187 # create a face from a single selected vertex, if it is an open vertex
188 def quad_from_vertex(bm, vert_sel, context, event):
189 addon_prefs = context.preferences.addons[__name__].preferences
190 ob = context.active_object
191 me = ob.data
192 region = context.region
193 region_3d = context.space_data.region_3d
195 # find linked edges that are open (<2 faces connected)
196 edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
197 if len(edges) < 2:
198 return
200 # determine which edges to use, based on mouse cursor position
201 min_dist = False
202 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
203 for a, b in itertools.combinations(edges, 2):
204 other_verts = [vert for edge in [a, b] for vert in edge.verts \
205 if not vert.select]
206 mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
208 new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
209 world_pos = ob.matrix_world @ new_pos
210 screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
211 world_pos)
212 dist = (mouse_pos - screen_pos).length
213 if not min_dist or dist < min_dist[0]:
214 min_dist = (dist, (a, b), other_verts, new_pos)
216 # create vertex at location mirrored in the line, connecting the open edges
217 edges = min_dist[1]
218 other_verts = min_dist[2]
219 new_pos = min_dist[3]
220 vert_new = bm.verts.new(new_pos)
222 # normal detection
223 flip_align = True
224 normal_edge = edges[0]
225 if not normal_edge.link_faces:
226 normal_edge = edges[1]
227 if not normal_edge.link_faces:
228 # no connected faces, so no need to flip the face normal
229 flip_align = False
230 if flip_align: # there is a face to which the normal can be aligned
231 ref_verts = [v for v in normal_edge.link_faces[0].verts]
232 if other_verts[0] in ref_verts:
233 va_1 = other_verts[0]
234 va_2 = vert_sel
235 else:
236 va_1 = vert_sel
237 va_2 = other_verts[1]
238 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
239 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
240 # reference verts are at start and end of the list -> shift list
241 ref_verts = ref_verts[1:] + [ref_verts[0]]
242 if ref_verts.index(va_1) > ref_verts.index(va_2):
243 # connected face has same normal direction, so don't flip
244 flip_align = False
246 # material index detection
247 ref_faces = vert_sel.link_faces
248 if not ref_faces:
249 mat_index = False
250 smooth = False
251 else:
252 mat_index = ref_faces[0].material_index
253 smooth = ref_faces[0].smooth
255 if addon_prefs.quad_from_v_mat:
256 mat_index = bpy.context.object.active_material_index
258 # create face between all 4 vertices involved
259 verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
260 if flip_align:
261 verts.reverse()
262 face = bm.faces.new(verts)
263 if mat_index:
264 face.material_index = mat_index
265 face.smooth = smooth
267 # change selection
268 vert_new.select = True
269 vert_sel.select = False
271 # adjust uv-map
272 if __name__ != '__main__':
273 if addon_prefs.adjustuv:
274 uv_layer = get_uv_layer(ob, bm, mat_index)
275 if uv_layer:
276 uv_others = {}
277 uv_sel = None
278 uv_new = None
279 # get original uv coordinates
280 for i in range(2):
281 for loop in other_verts[i].link_loops:
282 if loop.face.index > -1:
283 uv_others[loop.vert.index] = loop[uv_layer].uv
284 break
285 if len(uv_others) == 2:
286 mid_other = (list(uv_others.values())[0] +
287 list(uv_others.values())[1]) / 2
288 for loop in vert_sel.link_loops:
289 if loop.face.index > -1:
290 uv_sel = loop[uv_layer].uv
291 break
292 if uv_sel:
293 uv_new = 2 * (mid_other - uv_sel) + uv_sel
295 # set uv coordinates for new loops
296 if uv_new:
297 for loop in face.loops:
298 if loop.vert.index == -1:
299 x, y = uv_new
300 elif loop.vert.index in uv_others:
301 x, y = uv_others[loop.vert.index]
302 else:
303 x, y = uv_sel
304 loop[uv_layer].uv = (x, y)
306 # toggle mode, to force correct drawing
307 bpy.ops.object.mode_set(mode='OBJECT')
308 bpy.ops.object.mode_set(mode='EDIT')
311 def expand_vert(self, context, event):
312 addon_prefs = context.preferences.addons[__name__].preferences
313 ob = context.active_object
314 obj = bpy.context.object
315 me = obj.data
316 bm = bmesh.from_edit_mesh(me)
317 region = context.region
318 region_3d = context.space_data.region_3d
319 rv3d = context.space_data.region_3d
321 for v in bm.verts:
322 if v.select:
323 v_active = v
325 try:
326 depth_location = v_active.co
327 except:
328 return {'CANCELLED'}
329 # create vert in mouse cursor location
331 mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
332 location_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, depth_location)
334 c_verts = []
335 # find and select linked edges that are open (<2 faces connected) add those edge verts to c_verts list
336 linked = v_active.link_edges
337 for edges in linked:
338 if len(edges.link_faces) < 2:
339 edges.select = True
340 for v in edges.verts:
341 if v is not v_active:
342 c_verts.append(v)
344 # Compare distance in 2d between mouse and edges middle points
345 screen_pos_va = view3d_utils.location_3d_to_region_2d(region, region_3d,
346 ob.matrix_world @ v_active.co)
347 screen_pos_v1 = view3d_utils.location_3d_to_region_2d(region, region_3d,
348 ob.matrix_world @ c_verts[0].co)
349 screen_pos_v2 = view3d_utils.location_3d_to_region_2d(region, region_3d,
350 ob.matrix_world @ c_verts[1].co)
352 mid_pos_v1 = Vector(((screen_pos_va[0] + screen_pos_v1[0]) / 2, (screen_pos_va[1] + screen_pos_v1[1]) / 2))
353 mid_pos_V2 = Vector(((screen_pos_va[0] + screen_pos_v2[0]) / 2, (screen_pos_va[1] + screen_pos_v2[1]) / 2))
355 dist1 = math.log10(pow((mid_pos_v1[0] - mouse_pos[0]), 2) + pow((mid_pos_v1[1] - mouse_pos[1]), 2))
356 dist2 = math.log10(pow((mid_pos_V2[0] - mouse_pos[0]), 2) + pow((mid_pos_V2[1] - mouse_pos[1]), 2))
358 bm.normal_update()
359 bm.verts.ensure_lookup_table()
361 # Deselect not needed point and create new face
362 if dist1 < dist2:
363 c_verts[1].select = False
364 lleft = c_verts[0].link_faces
366 else:
367 c_verts[0].select = False
368 lleft = c_verts[1].link_faces
370 lactive = v_active.link_faces
371 # lverts = lactive[0].verts
373 mat_index = lactive[0].material_index
374 smooth = lactive[0].smooth
376 for faces in lactive:
377 if faces in lleft:
378 cface = faces
379 if len(faces.verts) == 3:
380 bm.normal_update()
381 bmesh.update_edit_mesh(obj.data)
382 bpy.ops.mesh.select_all(action='DESELECT')
383 v_active.select = True
384 bpy.ops.mesh.rip_edge_move('INVOKE_DEFAULT')
385 return {'FINISHED'}
387 lverts = cface.verts
389 # create triangle with correct normal orientation
390 # if You looking at that part - yeah... I know. I still dont get how blender calculates normals...
392 # from L to R
393 if dist1 < dist2:
394 if (lverts[0] == v_active and lverts[3] == c_verts[0]) \
395 or (lverts[2] == v_active and lverts[1] == c_verts[0]) \
396 or (lverts[1] == v_active and lverts[0] == c_verts[0]) \
397 or (lverts[3] == v_active and lverts[2] == c_verts[0]):
398 v_new = bm.verts.new(v_active.co)
399 face_new = bm.faces.new((c_verts[0], v_new, v_active))
401 elif (lverts[1] == v_active and lverts[2] == c_verts[0]) \
402 or (lverts[0] == v_active and lverts[1] == c_verts[0]) \
403 or (lverts[3] == v_active and lverts[0] == c_verts[0]) \
404 or (lverts[2] == v_active and lverts[3] == c_verts[0]):
405 v_new = bm.verts.new(v_active.co)
406 face_new = bm.faces.new((v_active, v_new, c_verts[0]))
408 else:
409 pass
410 # from R to L
411 else:
412 if (lverts[2] == v_active and lverts[3] == c_verts[1]) \
413 or (lverts[0] == v_active and lverts[1] == c_verts[1]) \
414 or (lverts[1] == v_active and lverts[2] == c_verts[1]) \
415 or (lverts[3] == v_active and lverts[0] == c_verts[1]):
416 v_new = bm.verts.new(v_active.co)
417 face_new = bm.faces.new((v_active, v_new, c_verts[1]))
419 elif (lverts[0] == v_active and lverts[3] == c_verts[1]) \
420 or (lverts[2] == v_active and lverts[1] == c_verts[1]) \
421 or (lverts[1] == v_active and lverts[0] == c_verts[1]) \
422 or (lverts[3] == v_active and lverts[2] == c_verts[1]):
423 v_new = bm.verts.new(v_active.co)
424 face_new = bm.faces.new((c_verts[1], v_new, v_active))
426 else:
427 pass
429 # set smooth and mat based on starting face
430 if addon_prefs.tris_from_v_mat:
431 face_new.material_index = bpy.context.object.active_material_index
432 else:
433 face_new.material_index = mat_index
434 face_new.smooth = smooth
436 # update normals
437 bpy.ops.mesh.select_all(action='DESELECT')
438 v_new.select = True
439 bm.select_history.add(v_new)
441 bm.normal_update()
442 bmesh.update_edit_mesh(obj.data)
443 bpy.ops.transform.translate('INVOKE_DEFAULT')
446 def checkforconnected(connection):
447 obj = bpy.context.object
448 me = obj.data
449 bm = bmesh.from_edit_mesh(me)
451 # Checks for number of edes or faces connected to selected vertex
452 for v in bm.verts:
453 if v.select:
454 v_active = v
455 if connection == 'faces':
456 linked = v_active.link_faces
457 elif connection == 'edges':
458 linked = v_active.link_edges
460 bmesh.update_edit_mesh(obj.data)
461 return len(linked)
464 # autograb preference in addons panel
465 class F2AddonPreferences(bpy.types.AddonPreferences):
466 bl_idname = __name__
467 adjustuv : bpy.props.BoolProperty(
468 name="Adjust UV",
469 description="Automatically update UV unwrapping",
470 default=False)
471 autograb : bpy.props.BoolProperty(
472 name="Auto Grab",
473 description="Automatically puts a newly created vertex in grab mode",
474 default=True)
475 extendvert : bpy.props.BoolProperty(
476 name="Enable Extend Vert",
477 description="Enables a way to build tris and quads by adding verts",
478 default=False)
479 quad_from_e_mat : bpy.props.BoolProperty(
480 name="Quad From Edge",
481 description="Use active material for created face instead of close one",
482 default=True)
483 quad_from_v_mat : bpy.props.BoolProperty(
484 name="Quad From Vert",
485 description="Use active material for created face instead of close one",
486 default=True)
487 tris_from_v_mat : bpy.props.BoolProperty(
488 name="Tris From Vert",
489 description="Use active material for created face instead of close one",
490 default=True)
491 ngons_v_mat : bpy.props.BoolProperty(
492 name="Ngons",
493 description="Use active material for created face instead of close one",
494 default=True)
496 def draw(self, context):
497 layout = self.layout
499 col = layout.column()
500 col.label(text="behaviours:")
501 col.prop(self, "autograb")
502 col.prop(self, "adjustuv")
503 col.prop(self, "extendvert")
505 col = layout.column()
506 col.label(text="use active material when creating:")
507 col.prop(self, "quad_from_e_mat")
508 col.prop(self, "quad_from_v_mat")
509 col.prop(self, "tris_from_v_mat")
510 col.prop(self, "ngons_v_mat")
513 class MeshF2(bpy.types.Operator):
514 """Tooltip"""
515 bl_idname = "mesh.f2"
516 bl_label = "Make Edge/Face"
517 bl_description = "Extends the 'Make Edge/Face' functionality"
518 bl_options = {'REGISTER', 'UNDO'}
520 @classmethod
521 def poll(cls, context):
522 # check we are in mesh editmode
523 ob = context.active_object
524 return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
526 def usequad(self, bm, sel, context, event):
527 quad_from_vertex(bm, sel, context, event)
528 if __name__ != '__main__':
529 addon_prefs = context.preferences.addons[__name__].preferences
530 if addon_prefs.autograb:
531 bpy.ops.transform.translate('INVOKE_DEFAULT')
533 def invoke(self, context, event):
534 bm = bmesh.from_edit_mesh(context.active_object.data)
535 sel = [v for v in bm.verts if v.select]
536 if len(sel) > 2:
537 # original 'Make Edge/Face' behaviour
538 try:
539 bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
540 addon_prefs = context.preferences.addons[__name__].preferences
541 if addon_prefs.ngons_v_mat:
542 bpy.ops.object.material_slot_assign()
543 except:
544 return {'CANCELLED'}
545 elif len(sel) == 1:
546 # single vertex selected -> mirror vertex and create new face
547 addon_prefs = context.preferences.addons[__name__].preferences
548 if addon_prefs.extendvert:
549 if checkforconnected('faces') in [2]:
550 if checkforconnected('edges') in [3]:
551 expand_vert(self, context, event)
552 else:
553 self.usequad(bm, sel[0], context, event)
555 elif checkforconnected('faces') in [1]:
556 if checkforconnected('edges') in [2]:
557 expand_vert(self, context, event)
558 else:
559 self.usequad(bm, sel[0], context, event)
560 else:
561 self.usequad(bm, sel[0], context, event)
562 else:
563 self.usequad(bm, sel[0], context, event)
564 elif len(sel) == 2:
565 edges_sel = [ed for ed in bm.edges if ed.select]
566 if len(edges_sel) != 1:
567 # 2 vertices selected, but not on the same edge
568 bpy.ops.mesh.edge_face_add()
569 else:
570 # single edge selected -> new face from linked open edges
571 quad_from_edge(bm, edges_sel[0], context, event)
573 return {'FINISHED'}
576 # registration
577 classes = [MeshF2, F2AddonPreferences]
578 addon_keymaps = []
581 def register():
582 # add operator
583 for c in classes:
584 bpy.utils.register_class(c)
586 # add keymap entry
587 kcfg = bpy.context.window_manager.keyconfigs.addon
588 if kcfg:
589 km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
590 kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
591 addon_keymaps.append((km, kmi.idname))
594 def unregister():
595 # remove keymap entry
596 for km, kmi_idname in addon_keymaps:
597 for kmi in km.keymap_items:
598 if kmi.idname == kmi_idname:
599 km.keymap_items.remove(kmi)
600 addon_keymaps.clear()
602 # remove operator and preferences
603 for c in reversed(classes):
604 bpy.utils.unregister_class(c)
607 if __name__ == "__main__":
608 register()