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