Update for border select -> box select rename
[blender-addons.git] / mesh_f2.py
blob88f503eae4bd5a1c76c95c16e6be705ed5d37941
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, 2),
26 "blender": (2, 70, 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 # returns a custom data layer of the UV map, or None
44 def get_uv_layer(ob, bm, mat_index):
45 uv = None
46 uv_layer = None
47 if not ob.material_slots:
48 me = ob.data
49 if me.uv_textures:
50 uv = me.uv_textures.active.name
51 else:
52 mat = ob.material_slots[mat_index].material
53 if mat is not None:
54 slot = mat.texture_slots[mat.active_texture_index]
55 if slot and slot.uv_layer:
56 uv = slot.uv_layer
57 else:
58 for tex_slot in mat.texture_slots:
59 if tex_slot and tex_slot.uv_layer:
60 uv = tex_slot.uv_layer
61 break
62 if uv:
63 uv_layer = bm.loops.layers.uv.get(uv)
65 return(uv_layer)
68 # create a face from a single selected edge
69 def quad_from_edge(bm, edge_sel, context, event):
70 ob = context.active_object
71 region = context.region
72 region_3d = context.space_data.region_3d
74 # find linked edges that are open (<2 faces connected) and not part of
75 # the face the selected edge belongs to
76 all_edges = [[edge for edge in edge_sel.verts[i].link_edges if \
77 len(edge.link_faces) < 2 and edge != edge_sel and \
78 sum([face in edge_sel.link_faces for face in edge.link_faces]) == 0] \
79 for i in range(2)]
80 if not all_edges[0] or not all_edges[1]:
81 return
83 # determine which edges to use, based on mouse cursor position
84 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
85 optimal_edges = []
86 for edges in all_edges:
87 min_dist = False
88 for edge in edges:
89 vert = [vert for vert in edge.verts if not vert.select][0]
90 world_pos = ob.matrix_world * vert.co.copy()
91 screen_pos = view3d_utils.location_3d_to_region_2d(region,
92 region_3d, world_pos)
93 dist = (mouse_pos - screen_pos).length
94 if not min_dist or dist < min_dist[0]:
95 min_dist = (dist, edge, vert)
96 optimal_edges.append(min_dist)
98 # determine the vertices, which make up the quad
99 v1 = edge_sel.verts[0]
100 v2 = edge_sel.verts[1]
101 edge_1 = optimal_edges[0][1]
102 edge_2 = optimal_edges[1][1]
103 v3 = optimal_edges[0][2]
104 v4 = optimal_edges[1][2]
106 # normal detection
107 flip_align = True
108 normal_edge = edge_1
109 if not normal_edge.link_faces:
110 normal_edge = edge_2
111 if not normal_edge.link_faces:
112 normal_edge = edge_sel
113 if not normal_edge.link_faces:
114 # no connected faces, so no need to flip the face normal
115 flip_align = False
116 if flip_align: # there is a face to which the normal can be aligned
117 ref_verts = [v for v in normal_edge.link_faces[0].verts]
118 if v3 in ref_verts:
119 va_1 = v3
120 va_2 = v1
121 elif normal_edge == edge_sel:
122 va_1 = v1
123 va_2 = v2
124 else:
125 va_1 = v2
126 va_2 = v4
127 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
128 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
129 # reference verts are at start and end of the list -> shift list
130 ref_verts = ref_verts[1:] + [ref_verts[0]]
131 if ref_verts.index(va_1) > ref_verts.index(va_2):
132 # connected face has same normal direction, so don't flip
133 flip_align = False
135 # material index detection
136 ref_faces = edge_sel.link_faces
137 if not ref_faces:
138 ref_faces = edge_sel.verts[0].link_faces
139 if not ref_faces:
140 ref_faces = edge_sel.verts[1].link_faces
141 if not ref_faces:
142 mat_index = False
143 smooth = False
144 else:
145 mat_index = ref_faces[0].material_index
146 smooth = ref_faces[0].smooth
148 # create quad
149 try:
150 if v3 == v4:
151 # triangle (usually at end of quad-strip
152 verts = [v3, v1, v2]
153 else:
154 # normal face creation
155 verts = [v3, v1, v2, v4]
156 if flip_align:
157 verts.reverse()
158 face = bm.faces.new(verts)
159 if mat_index:
160 face.material_index = mat_index
161 face.smooth = smooth
162 except:
163 # face already exists
164 return
166 # change selection
167 edge_sel.select = False
168 for vert in edge_sel.verts:
169 vert.select = False
170 for edge in face.edges:
171 if edge.index < 0:
172 edge.select = True
173 v3.select = True
174 v4.select = True
176 # adjust uv-map
177 if __name__ != '__main__':
178 addon_prefs = context.user_preferences.addons[__name__].preferences
179 if addon_prefs.adjustuv:
180 uv_layer = get_uv_layer(ob, bm, mat_index)
181 if uv_layer:
182 uv_ori = {}
183 for vert in [v1, v2, v3, v4]:
184 for loop in vert.link_loops:
185 if loop.face.index > -1:
186 uv_ori[loop.vert.index] = loop[uv_layer].uv
187 if len(uv_ori) == 4 or len(uv_ori) == 3:
188 for loop in face.loops:
189 loop[uv_layer].uv = uv_ori[loop.vert.index]
191 # toggle mode, to force correct drawing
192 bpy.ops.object.mode_set(mode='OBJECT')
193 bpy.ops.object.mode_set(mode='EDIT')
196 # create a face from a single selected vertex, if it is an open vertex
197 def quad_from_vertex(bm, vert_sel, context, event):
198 ob = context.active_object
199 me = ob.data
200 region = context.region
201 region_3d = context.space_data.region_3d
203 # find linked edges that are open (<2 faces connected)
204 edges = [edge for edge in vert_sel.link_edges if len(edge.link_faces) < 2]
205 if len(edges) < 2:
206 return
208 # determine which edges to use, based on mouse cursor position
209 min_dist = False
210 mouse_pos = mathutils.Vector([event.mouse_region_x, event.mouse_region_y])
211 for a, b in itertools.combinations(edges, 2):
212 other_verts = [vert for edge in [a, b] for vert in edge.verts \
213 if not vert.select]
214 mid_other = (other_verts[0].co.copy() + other_verts[1].co.copy()) \
216 new_pos = 2 * (mid_other - vert_sel.co.copy()) + vert_sel.co.copy()
217 world_pos = ob.matrix_world * new_pos
218 screen_pos = view3d_utils.location_3d_to_region_2d(region, region_3d,
219 world_pos)
220 dist = (mouse_pos - screen_pos).length
221 if not min_dist or dist < min_dist[0]:
222 min_dist = (dist, (a, b), other_verts, new_pos)
224 # create vertex at location mirrored in the line, connecting the open edges
225 edges = min_dist[1]
226 other_verts = min_dist[2]
227 new_pos = min_dist[3]
228 vert_new = bm.verts.new(new_pos)
230 # normal detection
231 flip_align = True
232 normal_edge = edges[0]
233 if not normal_edge.link_faces:
234 normal_edge = edges[1]
235 if not normal_edge.link_faces:
236 # no connected faces, so no need to flip the face normal
237 flip_align = False
238 if flip_align: # there is a face to which the normal can be aligned
239 ref_verts = [v for v in normal_edge.link_faces[0].verts]
240 if other_verts[0] in ref_verts:
241 va_1 = other_verts[0]
242 va_2 = vert_sel
243 else:
244 va_1 = vert_sel
245 va_2 = other_verts[1]
246 if (va_1 == ref_verts[0] and va_2 == ref_verts[-1]) or \
247 (va_2 == ref_verts[0] and va_1 == ref_verts[-1]):
248 # reference verts are at start and end of the list -> shift list
249 ref_verts = ref_verts[1:] + [ref_verts[0]]
250 if ref_verts.index(va_1) > ref_verts.index(va_2):
251 # connected face has same normal direction, so don't flip
252 flip_align = False
254 # material index detection
255 ref_faces = vert_sel.link_faces
256 if not ref_faces:
257 mat_index = False
258 smooth = False
259 else:
260 mat_index = ref_faces[0].material_index
261 smooth = ref_faces[0].smooth
263 # create face between all 4 vertices involved
264 verts = [other_verts[0], vert_sel, other_verts[1], vert_new]
265 if flip_align:
266 verts.reverse()
267 face = bm.faces.new(verts)
268 if mat_index:
269 face.material_index = mat_index
270 face.smooth = smooth
272 # change selection
273 vert_new.select = True
274 vert_sel.select = False
276 # adjust uv-map
277 if __name__ != '__main__':
278 addon_prefs = context.user_preferences.addons[__name__].preferences
279 if addon_prefs.adjustuv:
280 uv_layer = get_uv_layer(ob, bm, mat_index)
281 if uv_layer:
282 uv_others = {}
283 uv_sel = None
284 uv_new = None
285 # get original uv coordinates
286 for i in range(2):
287 for loop in other_verts[i].link_loops:
288 if loop.face.index > -1:
289 uv_others[loop.vert.index] = loop[uv_layer].uv
290 break
291 if len(uv_others) == 2:
292 mid_other = (list(uv_others.values())[0] +
293 list(uv_others.values())[1]) / 2
294 for loop in vert_sel.link_loops:
295 if loop.face.index > -1:
296 uv_sel = loop[uv_layer].uv
297 break
298 if uv_sel:
299 uv_new = 2 * (mid_other - uv_sel) + uv_sel
301 # set uv coordinates for new loops
302 if uv_new:
303 for loop in face.loops:
304 if loop.vert.index == -1:
305 x, y = uv_new
306 elif loop.vert.index in uv_others:
307 x, y = uv_others[loop.vert.index]
308 else:
309 x, y = uv_sel
310 loop[uv_layer].uv = (x, y)
312 # toggle mode, to force correct drawing
313 bpy.ops.object.mode_set(mode='OBJECT')
314 bpy.ops.object.mode_set(mode='EDIT')
317 # autograb preference in addons panel
318 class F2AddonPreferences(bpy.types.AddonPreferences):
319 bl_idname = __name__
320 adjustuv = bpy.props.BoolProperty(
321 name = "Adjust UV",
322 description = "Automatically update UV unwrapping",
323 default = True)
324 autograb = bpy.props.BoolProperty(
325 name = "Auto Grab",
326 description = "Automatically puts a newly created vertex in grab mode",
327 default = False)
329 def draw(self, context):
330 layout = self.layout
331 layout.prop(self, "autograb")
332 layout.prop(self, "adjustuv")
335 class MeshF2(bpy.types.Operator):
336 """Tooltip"""
337 bl_idname = "mesh.f2"
338 bl_label = "Make Edge/Face"
339 bl_description = "Extends the 'Make Edge/Face' functionality"
340 bl_options = {'REGISTER', 'UNDO'}
342 @classmethod
343 def poll(cls, context):
344 # check we are in mesh editmode
345 ob = context.active_object
346 return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
348 def invoke(self, context, event):
349 bm = bmesh.from_edit_mesh(context.active_object.data)
350 sel = [v for v in bm.verts if v.select]
351 if len(sel) > 2:
352 # original 'Make Edge/Face' behaviour
353 try:
354 bpy.ops.mesh.edge_face_add('INVOKE_DEFAULT')
355 except:
356 return {'CANCELLED'}
357 elif len(sel) == 1:
358 # single vertex selected -> mirror vertex and create new face
359 quad_from_vertex(bm, sel[0], context, event)
360 if __name__ != '__main__':
361 addon_prefs = context.user_preferences.addons[__name__].\
362 preferences
363 if addon_prefs.autograb:
364 bpy.ops.transform.translate('INVOKE_DEFAULT')
365 elif len(sel) == 2:
366 edges_sel = [ed for ed in bm.edges if ed.select]
367 if len(edges_sel) != 1:
368 # 2 vertices selected, but not on the same edge
369 bpy.ops.mesh.edge_face_add()
370 else:
371 # single edge selected -> new face from linked open edges
372 quad_from_edge(bm, edges_sel[0], context, event)
374 return {'FINISHED'}
377 # registration
378 classes = (
379 MeshF2,
380 F2AddonPreferences,
383 addon_keymaps = []
386 def register():
387 # add operator
388 for c in classes:
389 bpy.utils.register_class(c)
391 # add keymap entry
392 kcfg = bpy.context.window_manager.keyconfigs.addon
393 if kcfg:
394 km = kcfg.keymaps.new(name='Mesh', space_type='EMPTY')
395 kmi = km.keymap_items.new("mesh.f2", 'F', 'PRESS')
396 addon_keymaps.append((km, kmi))
399 def unregister():
400 # remove keymap entry
401 for km, kmi in addon_keymaps:
402 km.keymap_items.remove(kmi)
403 addon_keymaps.clear()
405 # remove operator and preferences
406 for c in reversed(classes):
407 bpy.utils.unregister_class(c)
410 if __name__ == "__main__":
411 register()