Update for API change: scene.cursor_location -> scene.cursor.location
[blender-addons.git] / mesh_f2.py
blob12b0ab653543b4374c4e5429db10a683eb839588
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 bl_info = {
22 "name": "F2",
23 "author": "Bart Crouch, Alexander Nedovizin, Paul Kotelevets "
24 "(concept design)",
25 "version": (1, 7, 3),
26 "blender": (2, 80, 0),
27 "location": "Editmode > F",
28 "warning": "",
29 "description": "Extends the 'Make Edge/Face' functionality",
30 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Modeling/F2",
32 "category": "Mesh",
36 import bmesh
37 import bpy
38 import itertools
39 import mathutils
40 from bpy_extras import view3d_utils
43 # create a face from a single selected edge
44 def quad_from_edge(bm, edge_sel, context, event):
45 ob = context.active_object
46 region = context.region
47 region_3d = context.space_data.region_3d
49 # find linked edges that are open (<2 faces connected) and not part of
50 # the face the selected edge belongs to
51 all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
52 len(edge.link_faces) < 2 and edge != edge_sel and \
53 sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
54 for i in range(2)]
55 if not all_edges[0] or not all_edges[1]:
56 return
58 # determine which edges to use, based on mouse cursor position
59 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
60 optimal_edges = []
61 for edges in all_edges:
62 min_dist = False
63 for edge in edges:
64 vert = [vert for vert in edge.verts if not vert.select][0]
65 world_pos = ob.matrix_world @ vert.co.copy()
66 screen_pos = view3d_utils.location_3d_to_region_2d(region,
67 region_3d, world_pos)
68 dist = (mouse_pos - screen_pos).length
69 if not min_dist or dist < min_dist[0]:
70 min_dist = (dist, edge, vert)
71 optimal_edges.append(min_dist)
73 # determine the vertices, which make up the quad
74 v1 = edge_sel.verts[0]
75 v2 = edge_sel.verts[1]
76 edge_1 = optimal_edges[0][1]
77 edge_2 = optimal_edges[1][1]
78 v3 = optimal_edges[0][2]
79 v4 = optimal_edges[1][2]
81 # normal detection
82 flip_align = True
83 normal_edge = edge_1
84 if not normal_edge.link_faces:
85 normal_edge = edge_2
86 if not normal_edge.link_faces:
87 normal_edge = edge_sel
88 if not normal_edge.link_faces:
89 # no connected faces, so no need to flip the face normal
90 flip_align = False
91 if flip_align: # there is a face to which the normal can be aligned
92 ref_verts = [v for v in normal_edge.link_faces[0].verts]
93 if v3 in ref_verts:
94 va_1 = v3
95 va_2 = v1
96 elif normal_edge == edge_sel:
97 va_1 = v1
98 va_2 = v2
99 else:
100 va_1 = v2
101 va_2 = v4
102 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
103 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
104 # reference verts are at start and end of the list -> shift list
105 ref_verts = ref_verts[1:] + [ref_verts[0]]
106 if ref_verts.index(va_1) > ref_verts.index(va_2):
107 # connected face has same normal direction, so don't flip
108 flip_align = False
110 # material index detection
111 ref_faces = edge_sel.link_faces
112 if not ref_faces:
113 ref_faces = edge_sel.verts[0].link_faces
114 if not ref_faces:
115 ref_faces = edge_sel.verts[1].link_faces
116 if not ref_faces:
117 mat_index = False
118 smooth = False
119 else:
120 mat_index = ref_faces[0].material_index
121 smooth = ref_faces[0].smooth
123 # create quad
124 try:
125 if v3 == v4:
126 # triangle (usually at end of quad-strip
127 verts = [v3, v1, v2]
128 else:
129 # normal face creation
130 verts = [v3, v1, v2, v4]
131 if flip_align:
132 verts.reverse()
133 face = bm.faces.new(verts)
134 if mat_index:
135 face.material_index = mat_index
136 face.smooth = smooth
137 except:
138 # face already exists
139 return
141 # change selection
142 edge_sel.select = False
143 for vert in edge_sel.verts:
144 vert.select = False
145 for edge in face.edges:
146 if edge.index < 0:
147 edge.select = True
148 v3.select = True
149 v4.select = True
151 # adjust uv-map
152 if __name__ != '__main__':
153 addon_prefs = context.preferences.addons[__name__].preferences
154 if addon_prefs.adjustuv:
155 for (key, uv_layer) in bm.loops.layers.uv.items():
156 uv_ori = {}
157 for vert in [v1, v2, v3, v4]:
158 for loop in vert.link_loops:
159 if loop.face.index > -1:
160 uv_ori[loop.vert.index] = loop[uv_layer].uv
161 if len(uv_ori) == 4 or len(uv_ori) == 3:
162 for loop in face.loops:
163 if loop.vert.index in uv_ori:
164 loop[uv_layer].uv = uv_ori[loop.vert.index]
166 # toggle mode, to force correct drawing
167 bpy.ops.object.mode_set(mode='OBJECT')
168 bpy.ops.object.mode_set(mode='EDIT')
171 # create a face from a single selected vertex, if it is an open vertex
172 def quad_from_vertex(bm, vert_sel, context, event):
173 ob = context.active_object
174 me = ob.data
175 region = context.region
176 region_3d = context.space_data.region_3d
178 # find linked edges that are open (<2 faces connected)
179 edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
180 if len(edges) < 2:
181 return
183 # determine which edges to use, based on mouse cursor position
184 min_dist = False
185 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
186 for a, b in itertools.combinations(edges, 2):
187 other_verts = [vert for edge in [a, b] for vert in edge.verts \
188 if not vert.select]
189 mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
191 new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
192 world_pos = ob.matrix_world @ new_pos
193 screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
194 world_pos)
195 dist = (mouse_pos - screen_pos).length
196 if not min_dist or dist < min_dist[0]:
197 min_dist = (dist, (a, b), other_verts, new_pos)
199 # create vertex at location mirrored in the line, connecting the open edges
200 edges = min_dist[1]
201 other_verts = min_dist[2]
202 new_pos = min_dist[3]
203 vert_new = bm.verts.new(new_pos)
205 # normal detection
206 flip_align = True
207 normal_edge = edges[0]
208 if not normal_edge.link_faces:
209 normal_edge = edges[1]
210 if not normal_edge.link_faces:
211 # no connected faces, so no need to flip the face normal
212 flip_align = False
213 if flip_align: # there is a face to which the normal can be aligned
214 ref_verts = [v for v in normal_edge.link_faces[0].verts]
215 if other_verts[0] in ref_verts:
216 va_1 = other_verts[0]
217 va_2 = vert_sel
218 else:
219 va_1 = vert_sel
220 va_2 = other_verts[1]
221 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
222 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
223 # reference verts are at start and end of the list -> shift list
224 ref_verts = ref_verts[1:] + [ref_verts[0]]
225 if ref_verts.index(va_1) > ref_verts.index(va_2):
226 # connected face has same normal direction, so don't flip
227 flip_align = False
229 # material index detection
230 ref_faces = vert_sel.link_faces
231 if not ref_faces:
232 mat_index = False
233 smooth = False
234 else:
235 mat_index = ref_faces[0].material_index
236 smooth = ref_faces[0].smooth
238 # create face between all 4 vertices involved
239 verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
240 if flip_align:
241 verts.reverse()
242 face = bm.faces.new(verts)
243 if mat_index:
244 face.material_index = mat_index
245 face.smooth = smooth
247 # change selection
248 vert_new.select = True
249 vert_sel.select = False
251 # adjust uv-map
252 if __name__ != '__main__':
253 addon_prefs = context.preferences.addons[__name__].preferences
254 if addon_prefs.adjustuv:
255 for (key, uv_layer) in bm.loops.layers.uv.items():
256 uv_others = {}
257 uv_sel = None
258 uv_new = None
259 # get original uv coordinates
260 for i in range(2):
261 for loop in other_verts[i].link_loops:
262 if loop.face.index > -1:
263 uv_others[loop.vert.index] = loop[uv_layer].uv
264 break
265 if len(uv_others) == 2:
266 mid_other = (list(uv_others.values())[0] +
267 list(uv_others.values())[1]) / 2
268 for loop in vert_sel.link_loops:
269 if loop.face.index > -1:
270 uv_sel = loop[uv_layer].uv
271 break
272 if uv_sel:
273 uv_new = 2 * (mid_other - uv_sel) + uv_sel
275 # set uv coordinates for new loops
276 if uv_new:
277 for loop in face.loops:
278 if loop.vert.index == -1:
279 x, y = uv_new
280 elif loop.vert.index in uv_others:
281 x, y = uv_others[loop.vert.index]
282 else:
283 x, y = uv_sel
284 loop[uv_layer].uv = (x, y)
286 # toggle mode, to force correct drawing
287 bpy.ops.object.mode_set(mode='OBJECT')
288 bpy.ops.object.mode_set(mode='EDIT')
291 # autograb preference in addons panel
292 class F2AddonPreferences(bpy.types.AddonPreferences):
293 bl_idname = __name__
294 adjustuv: bpy.props.BoolProperty(
295 name = "Adjust UV",
296 description = "Automatically update UV unwrapping",
297 default = True)
298 autograb: bpy.props.BoolProperty(
299 name = "Auto Grab",
300 description = "Automatically puts a newly created vertex in grab mode",
301 default = False)
303 def draw(self, context):
304 layout = self.layout
305 layout.prop(self, "autograb")
306 layout.prop(self, "adjustuv")
309 class MeshF2(bpy.types.Operator):
310 """Tooltip"""
311 bl_idname = "mesh.f2"
312 bl_label = "Make Edge/Face"
313 bl_description = "Extends the 'Make Edge/Face' functionality"
314 bl_options = {'REGISTER', 'UNDO'}
316 @classmethod
317 def poll(cls, context):
318 # check we are in mesh editmode
319 ob = context.active_object
320 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
322 def invoke(self, context, event):
323 bm = bmesh.from_edit_mesh(context.active_object.data)
324 sel = [v for v in bm.verts if v.select]
325 if len(sel) > 2:
326 # original 'Make Edge/Face' behaviour
327 try:
328 bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
329 except:
330 return {'CANCELLED'}
331 elif len(sel) == 1:
332 # single vertex selected -> mirror vertex and create new face
333 quad_from_vertex(bm, sel[0], context, event)
334 if __name__ != '__main__':
335 addon_prefs = context.preferences.addons[__name__].\
336 preferences
337 if addon_prefs.autograb:
338 bpy.ops.transform.translate('INVOKE_DEFAULT')
339 elif len(sel) == 2:
340 edges_sel = [ed for ed in bm.edges if ed.select]
341 if len(edges_sel) != 1:
342 # 2 vertices selected, but not on the same edge
343 bpy.ops.mesh.edge_face_add()
344 else:
345 # single edge selected -> new face from linked open edges
346 quad_from_edge(bm, edges_sel[0], context, event)
348 return {'FINISHED'}
351 # registration
352 classes = (
353 MeshF2,
354 F2AddonPreferences,
357 addon_keymaps = []
360 def register():
361 # add operator
362 for c in classes:
363 bpy.utils.register_class(c)
365 # add keymap entry
366 kcfg = bpy.context.window_manager.keyconfigs.addon
367 if kcfg:
368 km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
369 kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
370 addon_keymaps.append((km, kmi))
373 def unregister():
374 # remove keymap entry
375 for km, kmi in addon_keymaps:
376 km.keymap_items.remove(kmi)
377 addon_keymaps.clear()
379 # remove operator and preferences
380 for c in reversed(classes):
381 bpy.utils.unregister_class(c)
384 if __name__ == "__main__":
385 register()