Clean-up: bl_info['tracker_url'] updated to developer.blender.org, some minor other...
[blender-addons-contrib.git] / mesh_insert_edge_ring.py
1 # Simplified BSD License
2 #
3 # Copyright (c) 2012, Florian Meyer
4 # tstscr@web.de
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # 1. Redistributions of source code must retain the above copyright notice, this
11 #    list of conditions and the following disclaimer.
12 # 2. Redistributions in binary form must reproduce the above copyright notice,
13 #    this list of conditions and the following disclaimer in the documentation
14 #    and/or other materials provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
20 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27 bl_info = {
28     "name": "Insert Edge Ring",
29     "author": "tstscr (tstscr@web.de)",
30     "version": (1, 0),
31     "blender": (2, 64, 0),
32     "location": "View3D > Edge Specials > Insert edge ring (Ctrl Alt R)",
33     "description": "Insert an edge ring along the selected edge loop",
34     "warning": "",
35     "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
36         "Scripts/Mesh/Insert_Edge_Ring",
37     "tracker_url": "https://developer.blender.org/T32424",
38     "category": "Mesh"}
39
40
41 import bpy, bmesh, math
42 from bpy.types import Operator
43 from bpy.props import FloatProperty, BoolProperty, EnumProperty
44 from mathutils import Vector
45 from collections import deque
46 from bmesh.utils import vert_separate
47
48
49 def update(bme):
50     bme.verts.index_update()
51     bme.edges.index_update()
52
53 def selected_edges(component, invert=False):
54     def is_vert(vert):
55         if invert:
56             return [e for e in component.link_edges if not e.select]
57         return [e for e in component.link_edges if e.select]
58     if type(component) == bmesh.types.BMVert:
59         return is_vert(component)
60     if type(component) == bmesh.types.BMEdge:
61         edges = []
62         for vert in component.verts:
63             edges.extend(is_vert(vert))
64         if component in edges:
65             edges.remove(component)
66         return edges
67
68 def edge_loop_from(v_start):
69
70     def walk(vert, vert_loop=deque()):
71         #print('from', vert.index)
72         edges_select = selected_edges(vert)
73         #print('length edges_select', len(edges_select))
74         if not vert_loop:
75             #print('inserting %d into vert_loop' %vert.index)
76             vert_loop.append(vert)
77
78         for edge in edges_select:
79             other_vert = edge.other_vert(vert)
80             #print('other_vert %d' %other_vert.index)
81
82             edge_is_valid = True
83             if edge.is_boundary \
84             or other_vert in vert_loop \
85             or len(edges_select) > 2 \
86             or len(selected_edges(other_vert)) > 2:
87                 #print('is not valid')
88                 edge_is_valid = False
89
90             if edge_is_valid:
91                 if vert == vert_loop[-1]:
92                     #print('appending %d' %other_vert.index)
93                     vert_loop.append(other_vert)
94                 else:
95                     #print('prepending %d' %other_vert.index)
96                     vert_loop.appendleft(other_vert)
97
98                 walk(other_vert, vert_loop)
99
100         return vert_loop
101     #####################################
102     v_loop = walk(v_start)
103     #print('returning', [v.index for v in v_loop])
104     return v_loop
105
106
107 def collect_edge_loops(bme):
108     edge_loops = []
109     verts_to_consider = [v for v in bme.verts if v.select]
110
111     while verts_to_consider:
112
113         v_start = verts_to_consider[-1]
114         #print('\nverts_to_consider', [v.index for v in verts_to_consider])
115         edge_loop = edge_loop_from(v_start)
116         #update(bme)
117         #print('edge_loop', [v.index for v in edge_loop])
118         for v in edge_loop:
119             try:
120                 verts_to_consider.remove(v)
121             except:
122                 print('tried to remove vert %d from verts_to_consider. \
123                        Failed somehow' %v.index)
124
125         if len(edge_loop) >= 3:
126             edge_loops.append(edge_loop)
127         else:
128             for v in edge_loop:
129                 v.select = False
130
131     if not verts_to_consider:
132         #print('no more verts_to_consider')
133         pass
134
135     return edge_loops
136
137
138 def insert_edge_ring(self, context):
139     def split_edge_loop(vert_loop):
140         other_loop = deque()
141         new_loop = deque()
142         for vert in vert_loop:
143             #print('OPERATING ON VERT', vert.index)
144             edges = selected_edges(vert)
145             v_new = bmesh.utils.vert_separate(vert, edges)
146             #print('RIPPING vert %d into' %vert.index, [v.index for v in v_new][:], \
147             #       'along edges', [e.index for e in edges])
148             if not closed:
149                 if len(v_new) == 2:
150                     other_loop.append([v for v in v_new if v != vert][0])
151                 else:
152                     other_loop.append(vert)
153
154             if closed:
155                 if not new_loop:
156                     #print('start_new_loop')
157                     new_loop.append(v_new[0])
158                     other_loop.append(v_new[1])
159                 else:
160                     neighbours = [e.other_vert(v_new[0]) for e in v_new[0].link_edges]
161                     #print('neighbours', [n.index for n in neighbours])
162                     for n in neighbours:
163                         if n in new_loop and v_new[0] not in new_loop:
164                             #print('v_detect')
165                             new_loop.append(v_new[0])
166                             other_loop.append(v_new[1])
167                         if n in other_loop and v_new[0] not in other_loop:
168                             #print('v_not_detect')
169                             new_loop.append(v_new[1])
170                             other_loop.append(v_new[0])
171
172         return other_loop, new_loop
173
174     def move_verts(vert_loop, other_vert_loop):
175
176         ### Offsets ###
177         def calc_offsets():
178             #print('\nCALCULATING OFFSETS')
179             offset = {}
180             for i, vert in enumerate(vert_loop):
181                 edges_select = selected_edges(vert)
182                 edges_unselect = selected_edges(vert, invert=True)
183
184                 vert_opposite = other_vert_loop[i]
185                 edges_select_opposite = selected_edges(vert_opposite)
186                 edges_unselect_opposite = selected_edges(vert_opposite, invert=True)
187
188                 ### MESH END VERT
189                 if vert == other_vert_loop[0] or vert == other_vert_loop[-1]:
190                     #print('vert %d is start-end in middle of mesh, \
191                     #       does not need moving\n' %vert.index)
192                     continue
193
194                 ### BOUNDARY VERT
195                 if len(edges_select) == 1:
196                     #print('verts %d  %d are on boundary' \
197                     #%(vert.index, other_vert_loop[i].index))
198                     border_edge = [e for e in edges_unselect if e.is_boundary][0]
199                     off = (border_edge.other_vert(vert).co - vert.co).normalized()
200                     if self.direction == 'LEFT':
201                         off *= 0
202                     offset[vert] = off
203                     #opposite vert
204                     border_edge_opposite = [e for e in edges_unselect_opposite \
205                                             if e.is_boundary][0]
206                     off = (border_edge_opposite.other_vert(vert_opposite).co \
207                            - vert_opposite.co).normalized()
208                     if self.direction == 'RIGHT':
209                         off *= 0
210                     offset[vert_opposite] = off
211                     continue
212
213                 ### MIDDLE VERT
214                 if len(edges_select) == 2:
215                     #print('\nverts %d  %d are in middle of loop' \
216                     #%(vert.index, other_vert_loop[i].index))
217                     tangents = [e.calc_tangent(e.link_loops[0]) for e in edges_select]
218                     off = (tangents[0] + tangents[1]).normalized()
219                     angle = tangents[0].angle(tangents[1])
220                     if self.even:
221                         off += off * angle * 0.263910
222                     if self.direction == 'LEFT':
223                         off *= 0
224                     offset[vert] = off
225                     #opposite vert
226                     tangents = [e.calc_tangent(e.link_loops[0]) \
227                                 for e in edges_select_opposite]
228                     off = (tangents[0] + tangents[1]).normalized()
229                     #angle= tangents[0].angle(tangents[1])
230                     if self.even:
231                         off += off * angle * 0.263910
232                     if self.direction == 'RIGHT':
233                         off *= 0
234                     offset[vert_opposite] = off
235                     continue
236
237             return offset
238
239         ### Moving ###
240         def move(offsets):
241             #print('\nMOVING VERTS')
242             for vert in offsets:
243                 vert.co += offsets[vert] * self.distance
244
245         offsets = calc_offsets()
246         move(offsets)
247
248     def generate_new_geo(vert_loop, other_vert_loop):
249         #print('\nGENERATING NEW GEOMETRY')
250
251         for i, vert in enumerate(vert_loop):
252             if vert == other_vert_loop[i]:
253                 continue
254             edge_new = bme.edges.new([vert, other_vert_loop[i]])
255             edge_new.select = True
256
257         bpy.ops.mesh.edge_face_add()
258
259     #####################################################################################
260     #####################################################################################
261     #####################################################################################
262
263     bme = bmesh.from_edit_mesh(context.object.data)
264
265     ### COLLECT EDGE LOOPS ###
266     e_loops = collect_edge_loops(bme)
267
268     for e_loop in e_loops:
269
270         #check for closed loop - douple vert at start-end
271         closed = False
272         edges_select = selected_edges(e_loop[0])
273         for e in edges_select:
274             if e_loop[-1] in e.verts:
275                 closed = True
276
277         ### SPLITTING OF EDGES
278         other_vert_loop, new_loop = split_edge_loop(e_loop)
279         if closed:
280             e_loop = new_loop
281
282         ### MOVE RIPPED VERTS ###
283         move_verts(e_loop, other_vert_loop)
284
285         ### GENERATE NEW GEOMETRY ###
286         if self.generate_geo:
287             generate_new_geo(e_loop, other_vert_loop)
288
289     update(bme)
290
291 ###########################################################################
292 # OPERATOR
293 class MESH_OT_Insert_Edge_Ring(Operator):
294     """insert_edge_ring"""
295     bl_idname = "mesh.insert_edge_ring"
296     bl_label = "Insert edge ring"
297     bl_description = "Insert an edge ring along the selected edge loop"
298     bl_options = {'REGISTER', 'UNDO'}
299
300     distance = FloatProperty(
301             name="distance",
302             default=0.01,
303             min=0, soft_min=0,
304             precision=4,
305             description="distance to move verts from original location")
306
307     even = BoolProperty(
308             name='even',
309             default=True,
310             description='keep 90 degrees angles straight')
311
312     generate_geo = BoolProperty(
313             name='Generate Geo',
314             default=True,
315             description='Fill edgering with faces')
316
317     direction = EnumProperty(
318             name='direction',
319             description='Direction in which to expand the edge_ring',
320             items={
321             ('LEFT', '<|', 'only move verts left of loop (arbitrary)'),
322             ('CENTER', '<|>', 'move verts on both sides of loop'),
323             ('RIGHT', '|>', 'only move verts right of loop (arbitrary)'),
324             },
325             default='CENTER')
326
327     def draw(self, context):
328         layout = self.layout
329         col = layout.column(align=True)
330
331         col.prop(self, 'distance', slider=False)
332         col.prop(self, 'even', toggle=True)
333         col.prop(self, 'generate_geo', toggle=True)
334         col.separator()
335         col.label(text='Direction')
336         row = layout.row(align=True)
337         row.prop(self, 'direction', expand=True)
338
339     @classmethod
340     def poll(cls, context):
341         return context.mode == 'EDIT_MESH'
342
343     def execute(self, context):
344         #print('\nInserting edge ring')
345         insert_edge_ring(self, context)
346         return {'FINISHED'}
347
348 def insert_edge_ring_button(self, context):
349     self.layout.operator(MESH_OT_Insert_Edge_Ring.bl_idname,
350                          text="Insert edge ring")
351 ###########################################################################
352 # REGISTRATION
353
354 def register():
355     bpy.utils.register_module(__name__)
356     bpy.types.VIEW3D_MT_edit_mesh_edges.append(insert_edge_ring_button)
357
358     kc = bpy.context.window_manager.keyconfigs.addon
359     if kc:
360         km = kc.keymaps.new(name="3D View", space_type="VIEW_3D")
361         kmi = km.keymap_items.new('mesh.insert_edge_ring', \
362                                   'R', 'PRESS', ctrl=True, alt=True)
363
364 def unregister():
365     bpy.utils.unregister_module(__name__)
366     bpy.types.VIEW3D_MT_edit_mesh_edges.remove(insert_edge_ring_button)
367
368     kc = bpy.context.window_manager.keyconfigs.addon
369     if kc:
370         km = kc.keymaps["3D View"]
371         for kmi in km.keymap_items:
372             if kmi.idname == 'mesh.insert_edge_ring':
373                 km.keymap_items.remove(kmi)
374                 break
375
376 if __name__ == "__main__":
377     register()