~ubuntu-branches/ubuntu/trusty/blender/trusty

« back to all changes in this revision

Viewing changes to release/scripts/addons_contrib/mesh_looptools.py

  • Committer: Package Import Robot
  • Author(s): Matteo F. Vescovi
  • Date: 2012-05-12 20:02:22 UTC
  • mfrom: (14.2.16 sid)
  • Revision ID: package-import@ubuntu.com-20120512200222-lznjs2cxzaq96wua
Tags: 2.63a-1
* New upstream bugfix release
  + debian/patches/: re-worked since source code changed

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# ##### BEGIN GPL LICENSE BLOCK #####
2
 
#
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.
7
 
#
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.
12
 
#
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.
16
 
#
17
 
# ##### END GPL LICENSE BLOCK #####
18
 
 
19
 
bl_info = {
20
 
    'name': "LoopTools",
21
 
    'author': "Bart Crouch",
22
 
    'version': (3, 2, 4),
23
 
    'blender': (2, 6, 2),
24
 
    'location': "View3D > Toolbar and View3D > Specials (W-key)",
25
 
    'warning': "Bridge & Loft functions removed",
26
 
    'description': "Mesh modelling toolkit. Several tools to aid modelling",
27
 
    'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\
28
 
        "Scripts/Modeling/LoopTools",
29
 
    'tracker_url': "http://projects.blender.org/tracker/index.php?"\
30
 
        "func=detail&aid=26189",
31
 
    'category': 'Mesh'}
32
 
 
33
 
 
34
 
import bpy
35
 
import mathutils
36
 
import math
37
 
 
38
 
 
39
 
##########################################
40
 
####### General functions ################
41
 
##########################################
42
 
 
43
 
 
44
 
# used by all tools to improve speed on reruns
45
 
looptools_cache = {}
46
 
 
47
 
 
48
 
# force a full recalculation next time
49
 
def cache_delete(tool):
50
 
    if tool in looptools_cache:
51
 
        del looptools_cache[tool]
52
 
 
53
 
 
54
 
# check cache for stored information
55
 
def cache_read(tool, object, mesh, input_method, boundaries):
56
 
    # current tool not cached yet
57
 
    if tool not in looptools_cache:
58
 
        return(False, False, False, False, False)
59
 
    # check if selected object didn't change
60
 
    if object.name != looptools_cache[tool]["object"]:
61
 
        return(False, False, False, False, False)
62
 
    # check if input didn't change
63
 
    if input_method != looptools_cache[tool]["input_method"]:
64
 
        return(False, False, False, False, False)
65
 
    if boundaries != looptools_cache[tool]["boundaries"]:
66
 
        return(False, False, False, False, False)
67
 
    modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
68
 
        and mod.type == 'MIRROR']
69
 
    if modifiers != looptools_cache[tool]["modifiers"]:
70
 
        return(False, False, False, False, False)
71
 
    input = [v.index for v in mesh.vertices if v.select and not v.hide]
72
 
    if input != looptools_cache[tool]["input"]:
73
 
        return(False, False, False, False, False)
74
 
    # reading values
75
 
    single_loops = looptools_cache[tool]["single_loops"]
76
 
    loops = looptools_cache[tool]["loops"]
77
 
    derived = looptools_cache[tool]["derived"]
78
 
    mapping = looptools_cache[tool]["mapping"]
79
 
    
80
 
    return(True, single_loops, loops, derived, mapping)
81
 
 
82
 
 
83
 
# store information in the cache
84
 
def cache_write(tool, object, mesh, input_method, boundaries, single_loops,
85
 
loops, derived, mapping):
86
 
    # clear cache of current tool
87
 
    if tool in looptools_cache:
88
 
        del looptools_cache[tool]
89
 
    # prepare values to be saved to cache
90
 
    input = [v.index for v in mesh.vertices if v.select and not v.hide]
91
 
    modifiers = [mod.name for mod in object.modifiers if mod.show_viewport \
92
 
        and mod.type == 'MIRROR']
93
 
    # update cache
94
 
    looptools_cache[tool] = {"input": input, "object": object.name,
95
 
        "input_method": input_method, "boundaries": boundaries,
96
 
        "single_loops": single_loops, "loops": loops,
97
 
        "derived": derived, "mapping": mapping, "modifiers": modifiers}
98
 
 
99
 
 
100
 
# calculates natural cubic splines through all given knots
101
 
def calculate_cubic_splines(mesh_mod, tknots, knots):
102
 
    # hack for circular loops
103
 
    if knots[0] == knots[-1] and len(knots) > 1:
104
 
        circular = True
105
 
        k_new1 = []
106
 
        for k in range(-1, -5, -1):
107
 
            if k - 1 < -len(knots):
108
 
                k += len(knots)
109
 
            k_new1.append(knots[k-1])
110
 
        k_new2 = []
111
 
        for k in range(4):
112
 
            if k + 1 > len(knots) - 1:
113
 
                k -= len(knots)
114
 
            k_new2.append(knots[k+1])
115
 
        for k in k_new1:
116
 
            knots.insert(0, k)
117
 
        for k in k_new2:
118
 
            knots.append(k)
119
 
        t_new1 = []
120
 
        total1 = 0
121
 
        for t in range(-1, -5, -1):
122
 
            if t - 1 < -len(tknots):
123
 
                t += len(tknots)
124
 
            total1 += tknots[t] - tknots[t-1]
125
 
            t_new1.append(tknots[0] - total1)
126
 
        t_new2 = []
127
 
        total2 = 0
128
 
        for t in range(4):
129
 
            if t + 1 > len(tknots) - 1:
130
 
                t -= len(tknots)
131
 
            total2 += tknots[t+1] - tknots[t]
132
 
            t_new2.append(tknots[-1] + total2)
133
 
        for t in t_new1:
134
 
            tknots.insert(0, t)
135
 
        for t in t_new2:
136
 
            tknots.append(t)
137
 
    else:
138
 
        circular = False
139
 
    # end of hack
140
 
    
141
 
    n = len(knots)
142
 
    if n < 2:
143
 
        return False
144
 
    x = tknots[:]
145
 
    locs = [mesh_mod.vertices[k].co[:] for k in knots]
146
 
    result = []
147
 
    for j in range(3):
148
 
        a = []
149
 
        for i in locs:
150
 
            a.append(i[j])
151
 
        h = []
152
 
        for i in range(n-1):
153
 
            if x[i+1] - x[i] == 0:
154
 
                h.append(1e-8)
155
 
            else:
156
 
                h.append(x[i+1] - x[i])
157
 
        q = [False]
158
 
        for i in range(1, n-1):
159
 
            q.append(3/h[i]*(a[i+1]-a[i]) - 3/h[i-1]*(a[i]-a[i-1]))
160
 
        l = [1.0]
161
 
        u = [0.0]
162
 
        z = [0.0]
163
 
        for i in range(1, n-1):
164
 
            l.append(2*(x[i+1]-x[i-1]) - h[i-1]*u[i-1])
165
 
            if l[i] == 0:
166
 
                l[i] = 1e-8
167
 
            u.append(h[i] / l[i])
168
 
            z.append((q[i] - h[i-1] * z[i-1]) / l[i])
169
 
        l.append(1.0)
170
 
        z.append(0.0)
171
 
        b = [False for i in range(n-1)]
172
 
        c = [False for i in range(n)]
173
 
        d = [False for i in range(n-1)]
174
 
        c[n-1] = 0.0
175
 
        for i in range(n-2, -1, -1):
176
 
            c[i] = z[i] - u[i]*c[i+1]
177
 
            b[i] = (a[i+1]-a[i])/h[i] - h[i]*(c[i+1]+2*c[i])/3
178
 
            d[i] = (c[i+1]-c[i]) / (3*h[i])
179
 
        for i in range(n-1):
180
 
            result.append([a[i], b[i], c[i], d[i], x[i]])
181
 
    splines = []
182
 
    for i in range(len(knots)-1):
183
 
        splines.append([result[i], result[i+n-1], result[i+(n-1)*2]])
184
 
    if circular: # cleaning up after hack
185
 
        knots = knots[4:-4]
186
 
        tknots = tknots[4:-4]
187
 
    
188
 
    return(splines)
189
 
 
190
 
 
191
 
# calculates linear splines through all given knots
192
 
def calculate_linear_splines(mesh_mod, tknots, knots):
193
 
    splines = []
194
 
    for i in range(len(knots)-1):
195
 
        a = mesh_mod.vertices[knots[i]].co
196
 
        b = mesh_mod.vertices[knots[i+1]].co
197
 
        d = b-a
198
 
        t = tknots[i]
199
 
        u = tknots[i+1]-t
200
 
        splines.append([a, d, t, u]) # [locStart, locDif, tStart, tDif]
201
 
    
202
 
    return(splines)
203
 
 
204
 
 
205
 
# calculate a best-fit plane to the given vertices
206
 
def calculate_plane(mesh_mod, loop, method="best_fit", object=False):
207
 
    # getting the vertex locations
208
 
    locs = [mesh_mod.vertices[v].co.copy() for v in loop[0]]
209
 
    
210
 
    # calculating the center of masss
211
 
    com = mathutils.Vector()
212
 
    for loc in locs:
213
 
        com += loc
214
 
    com /= len(locs)
215
 
    x, y, z = com
216
 
    
217
 
    if method == 'best_fit':
218
 
        # creating the covariance matrix
219
 
        mat = mathutils.Matrix(((0.0, 0.0, 0.0),
220
 
                                (0.0, 0.0, 0.0),
221
 
                                (0.0, 0.0, 0.0),
222
 
                                ))
223
 
        for loc in locs:
224
 
            mat[0][0] += (loc[0]-x)**2
225
 
            mat[1][0] += (loc[0]-x)*(loc[1]-y)
226
 
            mat[2][0] += (loc[0]-x)*(loc[2]-z)
227
 
            mat[0][1] += (loc[1]-y)*(loc[0]-x)
228
 
            mat[1][1] += (loc[1]-y)**2
229
 
            mat[2][1] += (loc[1]-y)*(loc[2]-z)
230
 
            mat[0][2] += (loc[2]-z)*(loc[0]-x)
231
 
            mat[1][2] += (loc[2]-z)*(loc[1]-y)
232
 
            mat[2][2] += (loc[2]-z)**2
233
 
        
234
 
        # calculating the normal to the plane
235
 
        normal = False
236
 
        try:
237
 
            mat.invert()
238
 
        except:
239
 
            if sum(mat[0]) == 0.0:
240
 
                normal = mathutils.Vector((1.0, 0.0, 0.0))
241
 
            elif sum(mat[1]) == 0.0:
242
 
                normal = mathutils.Vector((0.0, 1.0, 0.0))
243
 
            elif sum(mat[2]) == 0.0:
244
 
                normal = mathutils.Vector((0.0, 0.0, 1.0))
245
 
        if not normal:
246
 
            # warning! this is different from .normalize()
247
 
            itermax = 500
248
 
            iter = 0
249
 
            vec = mathutils.Vector((1.0, 1.0, 1.0))
250
 
            vec2 = (mat * vec)/(mat * vec).length
251
 
            while vec != vec2 and iter<itermax:
252
 
                iter+=1
253
 
                vec = vec2
254
 
                vec2 = mat * vec
255
 
                if vec2.length != 0:
256
 
                    vec2 /= vec2.length
257
 
            if vec2.length == 0:
258
 
                vec2 = mathutils.Vector((1.0, 1.0, 1.0))
259
 
            normal = vec2
260
 
    
261
 
    elif method == 'normal':
262
 
        # averaging the vertex normals
263
 
        v_normals = [mesh_mod.vertices[v].normal for v in loop[0]]
264
 
        normal = mathutils.Vector()
265
 
        for v_normal in v_normals:
266
 
            normal += v_normal
267
 
        normal /= len(v_normals)
268
 
        normal.normalize()
269
 
        
270
 
    elif method == 'view':
271
 
        # calculate view normal
272
 
        rotation = bpy.context.space_data.region_3d.view_matrix.to_3x3().\
273
 
            inverted()
274
 
        normal = rotation * mathutils.Vector((0.0, 0.0, 1.0))
275
 
        if object:
276
 
            normal = object.matrix_world.inverted().to_euler().to_matrix() * \
277
 
                     normal
278
 
    
279
 
    return(com, normal)
280
 
 
281
 
 
282
 
# calculate splines based on given interpolation method (controller function)
283
 
def calculate_splines(interpolation, mesh_mod, tknots, knots):
284
 
    if interpolation == 'cubic':
285
 
        splines = calculate_cubic_splines(mesh_mod, tknots, knots[:])
286
 
    else: # interpolations == 'linear'
287
 
        splines = calculate_linear_splines(mesh_mod, tknots, knots[:])
288
 
    
289
 
    return(splines)
290
 
 
291
 
 
292
 
# check loops and only return valid ones
293
 
def check_loops(loops, mapping, mesh_mod):
294
 
    valid_loops = []
295
 
    for loop, circular in loops:
296
 
        # loop needs to have at least 3 vertices
297
 
        if len(loop) < 3:
298
 
            continue
299
 
        # loop needs at least 1 vertex in the original, non-mirrored mesh
300
 
        if mapping:
301
 
            all_virtual = True
302
 
            for vert in loop:
303
 
                if mapping[vert] > -1:
304
 
                    all_virtual = False
305
 
                    break
306
 
            if all_virtual:
307
 
                continue
308
 
        # vertices can not all be at the same location
309
 
        stacked = True
310
 
        for i in range(len(loop) - 1):
311
 
            if (mesh_mod.vertices[loop[i]].co - \
312
 
            mesh_mod.vertices[loop[i+1]].co).length > 1e-6:
313
 
                stacked = False
314
 
                break
315
 
        if stacked:
316
 
            continue    
317
 
        # passed all tests, loop is valid
318
 
        valid_loops.append([loop, circular])
319
 
    
320
 
    return(valid_loops)
321
 
 
322
 
 
323
 
# input: mesh, output: dict with the edge-key as key and face-index as value
324
 
def dict_edge_faces(mesh):
325
 
    edge_faces = dict([[edge.key, []] for edge in mesh.edges if not edge.hide])
326
 
    for face in mesh.tessfaces:
327
 
        if face.hide:
328
 
            continue
329
 
        for key in face.edge_keys:
330
 
            edge_faces[key].append(face.index)
331
 
    
332
 
    return(edge_faces)
333
 
 
334
 
# input: mesh (edge-faces optional), output: dict with face-face connections
335
 
def dict_face_faces(mesh, edge_faces=False):
336
 
    if not edge_faces:
337
 
        edge_faces = dict_edge_faces(mesh)
338
 
    
339
 
    connected_faces = dict([[face.index, []] for face in mesh.tessfaces if \
340
 
        not face.hide])
341
 
    for face in mesh.tessfaces:
342
 
        if face.hide:
343
 
            continue
344
 
        for edge_key in face.edge_keys:
345
 
            for connected_face in edge_faces[edge_key]:
346
 
                if connected_face == face.index:
347
 
                    continue
348
 
                connected_faces[face.index].append(connected_face)
349
 
    
350
 
    return(connected_faces)
351
 
 
352
 
 
353
 
# input: mesh, output: dict with the vert index as key and edge-keys as value
354
 
def dict_vert_edges(mesh):
355
 
    vert_edges = dict([[v.index, []] for v in mesh.vertices if not v.hide])
356
 
    for edge in mesh.edges:
357
 
        if edge.hide:
358
 
            continue
359
 
        for vert in edge.key:
360
 
            vert_edges[vert].append(edge.key)
361
 
    
362
 
    return(vert_edges)
363
 
 
364
 
 
365
 
# input: mesh, output: dict with the vert index as key and face index as value
366
 
def dict_vert_faces(mesh):
367
 
    vert_faces = dict([[v.index, []] for v in mesh.vertices if not v.hide])
368
 
    for face in mesh.tessfaces:
369
 
        if not face.hide:
370
 
            for vert in face.vertices:
371
 
                vert_faces[vert].append(face.index)
372
 
                
373
 
    return(vert_faces)
374
 
 
375
 
 
376
 
# input: list of edge-keys, output: dictionary with vertex-vertex connections
377
 
def dict_vert_verts(edge_keys):
378
 
    # create connection data
379
 
    vert_verts = {}
380
 
    for ek in edge_keys:
381
 
        for i in range(2):
382
 
            if ek[i] in vert_verts:
383
 
                vert_verts[ek[i]].append(ek[1-i])
384
 
            else:
385
 
                vert_verts[ek[i]] = [ek[1-i]]
386
 
    
387
 
    return(vert_verts)
388
 
 
389
 
 
390
 
# calculate input loops
391
 
def get_connected_input(object, mesh, scene, input):
392
 
    # get mesh with modifiers applied
393
 
    derived, mesh_mod = get_derived_mesh(object, mesh, scene)
394
 
    
395
 
    # calculate selected loops
396
 
    edge_keys = [edge.key for edge in mesh_mod.edges if \
397
 
        edge.select and not edge.hide]
398
 
    loops = get_connected_selections(edge_keys)
399
 
    
400
 
    # if only selected loops are needed, we're done
401
 
    if input == 'selected':
402
 
        return(derived, mesh_mod, loops)
403
 
    # elif input == 'all':    
404
 
    loops = get_parallel_loops(mesh_mod, loops)
405
 
    
406
 
    return(derived, mesh_mod, loops)
407
 
 
408
 
 
409
 
# sorts all edge-keys into a list of loops
410
 
def get_connected_selections(edge_keys):
411
 
    # create connection data
412
 
    vert_verts = dict_vert_verts(edge_keys)
413
 
    
414
 
    # find loops consisting of connected selected edges
415
 
    loops = []
416
 
    while len(vert_verts) > 0:
417
 
        loop = [iter(vert_verts.keys()).__next__()]
418
 
        growing = True
419
 
        flipped = False
420
 
        
421
 
        # extend loop
422
 
        while growing:
423
 
            # no more connection data for current vertex
424
 
            if loop[-1] not in vert_verts:
425
 
                if not flipped:
426
 
                    loop.reverse()
427
 
                    flipped = True
428
 
                else:
429
 
                    growing = False
430
 
            else:
431
 
                extended = False
432
 
                for i, next_vert in enumerate(vert_verts[loop[-1]]):
433
 
                    if next_vert not in loop:
434
 
                        vert_verts[loop[-1]].pop(i)
435
 
                        if len(vert_verts[loop[-1]]) == 0:
436
 
                            del vert_verts[loop[-1]]
437
 
                        # remove connection both ways
438
 
                        if next_vert in vert_verts:
439
 
                            if len(vert_verts[next_vert]) == 1:
440
 
                                del vert_verts[next_vert]
441
 
                            else:
442
 
                                vert_verts[next_vert].remove(loop[-1])
443
 
                        loop.append(next_vert)
444
 
                        extended = True
445
 
                        break
446
 
                if not extended:
447
 
                    # found one end of the loop, continue with next
448
 
                    if not flipped:
449
 
                        loop.reverse()
450
 
                        flipped = True
451
 
                    # found both ends of the loop, stop growing
452
 
                    else:
453
 
                        growing = False
454
 
        
455
 
        # check if loop is circular
456
 
        if loop[0] in vert_verts:
457
 
            if loop[-1] in vert_verts[loop[0]]:
458
 
                # is circular
459
 
                if len(vert_verts[loop[0]]) == 1:
460
 
                    del vert_verts[loop[0]]
461
 
                else:
462
 
                    vert_verts[loop[0]].remove(loop[-1])
463
 
                if len(vert_verts[loop[-1]]) == 1:
464
 
                    del vert_verts[loop[-1]]
465
 
                else:
466
 
                    vert_verts[loop[-1]].remove(loop[0])
467
 
                loop = [loop, True]
468
 
            else:
469
 
                # not circular
470
 
                loop = [loop, False]
471
 
        else:
472
 
            # not circular
473
 
            loop = [loop, False]
474
 
        
475
 
        loops.append(loop)
476
 
    
477
 
    return(loops)
478
 
 
479
 
 
480
 
# get the derived mesh data, if there is a mirror modifier
481
 
def get_derived_mesh(object, mesh, scene):
482
 
    # check for mirror modifiers
483
 
    if 'MIRROR' in [mod.type for mod in object.modifiers if mod.show_viewport]:
484
 
        derived = True
485
 
        # disable other modifiers
486
 
        show_viewport = [mod.name for mod in object.modifiers if \
487
 
            mod.show_viewport]
488
 
        for mod in object.modifiers:
489
 
            if mod.type != 'MIRROR':
490
 
                mod.show_viewport = False
491
 
        # get derived mesh
492
 
        mesh_mod = object.to_mesh(scene, True, 'PREVIEW')
493
 
        # re-enable other modifiers
494
 
        for mod_name in show_viewport:
495
 
            object.modifiers[mod_name].show_viewport = True
496
 
    # no mirror modifiers, so no derived mesh necessary
497
 
    else:
498
 
        derived = False
499
 
        mesh_mod = mesh
500
 
    
501
 
    return(derived, mesh_mod)
502
 
 
503
 
 
504
 
# return a mapping of derived indices to indices
505
 
def get_mapping(derived, mesh, mesh_mod, single_vertices, full_search, loops):
506
 
    if not derived:
507
 
        return(False)
508
 
    
509
 
    if full_search:
510
 
        verts = [v for v in mesh.vertices if not v.hide]
511
 
    else:
512
 
        verts = [v for v in mesh.vertices if v.select and not v.hide]
513
 
    
514
 
    # non-selected vertices around single vertices also need to be mapped
515
 
    if single_vertices:
516
 
        mapping = dict([[vert, -1] for vert in single_vertices])
517
 
        verts_mod = [mesh_mod.vertices[vert] for vert in single_vertices]
518
 
        for v in verts:
519
 
            for v_mod in verts_mod:
520
 
                if (v.co - v_mod.co).length < 1e-6:
521
 
                    mapping[v_mod.index] = v.index
522
 
                    break
523
 
        real_singles = [v_real for v_real in mapping.values() if v_real>-1]
524
 
        
525
 
        verts_indices = [vert.index for vert in verts]
526
 
        for face in [face for face in mesh.tessfaces if not face.select \
527
 
        and not face.hide]:
528
 
            for vert in face.vertices:
529
 
                if vert in real_singles:
530
 
                    for v in face.vertices:
531
 
                        if not v in verts_indices:
532
 
                            if mesh.vertices[v] not in verts:
533
 
                                verts.append(mesh.vertices[v])
534
 
                    break
535
 
    
536
 
    # create mapping of derived indices to indices
537
 
    mapping = dict([[vert, -1] for loop in loops for vert in loop[0]])
538
 
    if single_vertices:
539
 
        for single in single_vertices:
540
 
            mapping[single] = -1
541
 
    verts_mod = [mesh_mod.vertices[i] for i in mapping.keys()]
542
 
    for v in verts:
543
 
        for v_mod in verts_mod:
544
 
            if (v.co - v_mod.co).length < 1e-6:
545
 
                mapping[v_mod.index] = v.index
546
 
                verts_mod.remove(v_mod)
547
 
                break
548
 
    
549
 
    return(mapping)
550
 
 
551
 
 
552
 
# returns a list of all loops parallel to the input, input included
553
 
def get_parallel_loops(mesh_mod, loops):
554
 
    # get required dictionaries
555
 
    edge_faces = dict_edge_faces(mesh_mod)
556
 
    connected_faces = dict_face_faces(mesh_mod, edge_faces)
557
 
    # turn vertex loops into edge loops
558
 
    edgeloops = []
559
 
    for loop in loops:
560
 
        edgeloop = [[sorted([loop[0][i], loop[0][i+1]]) for i in \
561
 
            range(len(loop[0])-1)], loop[1]]
562
 
        if loop[1]: # circular
563
 
            edgeloop[0].append(sorted([loop[0][-1], loop[0][0]]))
564
 
        edgeloops.append(edgeloop[:])
565
 
    # variables to keep track while iterating
566
 
    all_edgeloops = []
567
 
    has_branches = False
568
 
    
569
 
    for loop in edgeloops:
570
 
        # initialise with original loop
571
 
        all_edgeloops.append(loop[0])
572
 
        newloops = [loop[0]]
573
 
        verts_used = []
574
 
        for edge in loop[0]:
575
 
            if edge[0] not in verts_used:
576
 
                verts_used.append(edge[0])
577
 
            if edge[1] not in verts_used:
578
 
                verts_used.append(edge[1])
579
 
        
580
 
        # find parallel loops
581
 
        while len(newloops) > 0:
582
 
            side_a = []
583
 
            side_b = []
584
 
            for i in newloops[-1]:
585
 
                i = tuple(i)
586
 
                forbidden_side = False
587
 
                if not i in edge_faces:
588
 
                    # weird input with branches
589
 
                    has_branches = True
590
 
                    break
591
 
                for face in edge_faces[i]:
592
 
                    if len(side_a) == 0 and forbidden_side != "a":
593
 
                        side_a.append(face)
594
 
                        if forbidden_side:
595
 
                            break
596
 
                        forbidden_side = "a"
597
 
                        continue
598
 
                    elif side_a[-1] in connected_faces[face] and \
599
 
                    forbidden_side != "a":
600
 
                        side_a.append(face)
601
 
                        if forbidden_side:
602
 
                            break
603
 
                        forbidden_side = "a"
604
 
                        continue
605
 
                    if len(side_b) == 0 and forbidden_side != "b":
606
 
                        side_b.append(face)
607
 
                        if forbidden_side:
608
 
                            break
609
 
                        forbidden_side = "b"
610
 
                        continue
611
 
                    elif side_b[-1] in connected_faces[face] and \
612
 
                    forbidden_side != "b":
613
 
                        side_b.append(face)
614
 
                        if forbidden_side:
615
 
                            break
616
 
                        forbidden_side = "b"
617
 
                        continue
618
 
            
619
 
            if has_branches:
620
 
                # weird input with branches
621
 
                break
622
 
            
623
 
            newloops.pop(-1)
624
 
            sides = []
625
 
            if side_a:
626
 
                sides.append(side_a)
627
 
            if side_b:
628
 
                sides.append(side_b)
629
 
            
630
 
            for side in sides:
631
 
                extraloop = []
632
 
                for fi in side:
633
 
                    for key in mesh_mod.tessfaces[fi].edge_keys:
634
 
                        if key[0] not in verts_used and key[1] not in \
635
 
                        verts_used:
636
 
                            extraloop.append(key)
637
 
                            break
638
 
                if extraloop:
639
 
                    for key in extraloop:
640
 
                        for new_vert in key:
641
 
                            if new_vert not in verts_used:
642
 
                                verts_used.append(new_vert)
643
 
                    newloops.append(extraloop)
644
 
                    all_edgeloops.append(extraloop)
645
 
    
646
 
    # input contains branches, only return selected loop
647
 
    if has_branches:
648
 
        return(loops)
649
 
    
650
 
    # change edgeloops into normal loops
651
 
    loops = []
652
 
    for edgeloop in all_edgeloops:
653
 
        loop = []
654
 
        # grow loop by comparing vertices between consecutive edge-keys
655
 
        for i in range(len(edgeloop)-1):
656
 
            for vert in range(2):
657
 
                if edgeloop[i][vert] in edgeloop[i+1]:
658
 
                    loop.append(edgeloop[i][vert])
659
 
                    break
660
 
        if loop:
661
 
            # add starting vertex
662
 
            for vert in range(2):
663
 
                if edgeloop[0][vert] != loop[0]:
664
 
                    loop = [edgeloop[0][vert]] + loop
665
 
                    break
666
 
            # add ending vertex
667
 
            for vert in range(2):
668
 
                if edgeloop[-1][vert] != loop[-1]:
669
 
                    loop.append(edgeloop[-1][vert])
670
 
                    break
671
 
            # check if loop is circular
672
 
            if loop[0] == loop[-1]:
673
 
                circular = True
674
 
                loop = loop[:-1]
675
 
            else:
676
 
                circular = False
677
 
        loops.append([loop, circular])
678
 
    
679
 
    return(loops)
680
 
 
681
 
 
682
 
# gather initial data
683
 
def initialise():
684
 
    global_undo = bpy.context.user_preferences.edit.use_global_undo
685
 
    bpy.context.user_preferences.edit.use_global_undo = False
686
 
    bpy.ops.object.mode_set(mode='OBJECT')
687
 
    object = bpy.context.active_object
688
 
    mesh = bpy.context.active_object.data
689
 
    
690
 
    return(global_undo, object, mesh)
691
 
 
692
 
 
693
 
# move the vertices to their new locations
694
 
def move_verts(mesh, mapping, move, influence):
695
 
    for loop in move:
696
 
        for index, loc in loop:
697
 
            if mapping:
698
 
                if mapping[index] == -1:
699
 
                    continue
700
 
                else:
701
 
                    index = mapping[index]
702
 
            if influence >= 0:
703
 
                mesh.vertices[index].co = loc*(influence/100) + \
704
 
                    mesh.vertices[index].co*((100-influence)/100)
705
 
            else:
706
 
                mesh.vertices[index].co = loc
707
 
 
708
 
 
709
 
# load custom tool settings 
710
 
def settings_load(self):
711
 
    lt = bpy.context.window_manager.looptools
712
 
    tool = self.name.split()[0].lower()
713
 
    keys = self.as_keywords().keys()
714
 
    for key in keys:
715
 
        setattr(self, key, getattr(lt, tool + "_" + key))
716
 
 
717
 
 
718
 
# store custom tool settings
719
 
def settings_write(self):
720
 
    lt = bpy.context.window_manager.looptools
721
 
    tool = self.name.split()[0].lower()
722
 
    keys = self.as_keywords().keys()
723
 
    for key in keys:
724
 
        setattr(lt, tool + "_" + key, getattr(self, key))
725
 
 
726
 
 
727
 
# clean up and set settings back to original state
728
 
def terminate(global_undo):
729
 
    bpy.ops.object.mode_set(mode='EDIT')
730
 
    bpy.context.user_preferences.edit.use_global_undo = global_undo
731
 
 
732
 
 
733
 
##########################################
734
 
####### Bridge functions #################
735
 
##########################################
736
 
 
737
 
# calculate a cubic spline through the middle section of 4 given coordinates
738
 
def bridge_calculate_cubic_spline(mesh, coordinates):
739
 
    result = []
740
 
    x = [0, 1, 2, 3]
741
 
    
742
 
    for j in range(3):
743
 
        a = []
744
 
        for i in coordinates:
745
 
            a.append(float(i[j]))
746
 
        h = []
747
 
        for i in range(3):
748
 
            h.append(x[i+1]-x[i])
749
 
        q = [False]
750
 
        for i in range(1,3):
751
 
            q.append(3.0/h[i]*(a[i+1]-a[i])-3.0/h[i-1]*(a[i]-a[i-1]))
752
 
        l = [1.0]
753
 
        u = [0.0]
754
 
        z = [0.0]
755
 
        for i in range(1,3):
756
 
            l.append(2.0*(x[i+1]-x[i-1])-h[i-1]*u[i-1])
757
 
            u.append(h[i]/l[i])
758
 
            z.append((q[i]-h[i-1]*z[i-1])/l[i])
759
 
        l.append(1.0)
760
 
        z.append(0.0)
761
 
        b = [False for i in range(3)]
762
 
        c = [False for i in range(4)]
763
 
        d = [False for i in range(3)]
764
 
        c[3] = 0.0
765
 
        for i in range(2,-1,-1):
766
 
            c[i] = z[i]-u[i]*c[i+1]
767
 
            b[i] = (a[i+1]-a[i])/h[i]-h[i]*(c[i+1]+2.0*c[i])/3.0
768
 
            d[i] = (c[i+1]-c[i])/(3.0*h[i])
769
 
        for i in range(3):
770
 
            result.append([a[i], b[i], c[i], d[i], x[i]])
771
 
    spline = [result[1], result[4], result[7]]
772
 
 
773
 
    return(spline)
774
 
 
775
 
 
776
 
# return a list with new vertex location vectors, a list with face vertex 
777
 
# integers, and the highest vertex integer in the virtual mesh
778
 
def bridge_calculate_geometry(mesh, lines, vertex_normals, segments,
779
 
interpolation, cubic_strength, min_width, max_vert_index):
780
 
    new_verts = []
781
 
    faces = []
782
 
    
783
 
    # calculate location based on interpolation method
784
 
    def get_location(line, segment, splines):
785
 
        v1 = mesh.vertices[lines[line][0]].co
786
 
        v2 = mesh.vertices[lines[line][1]].co
787
 
        if interpolation == 'linear':
788
 
            return v1 + (segment/segments) * (v2-v1)
789
 
        else: # interpolation == 'cubic'
790
 
            m = (segment/segments)
791
 
            ax,bx,cx,dx,tx = splines[line][0]
792
 
            x = ax+bx*m+cx*m**2+dx*m**3
793
 
            ay,by,cy,dy,ty = splines[line][1]
794
 
            y = ay+by*m+cy*m**2+dy*m**3
795
 
            az,bz,cz,dz,tz = splines[line][2]
796
 
            z = az+bz*m+cz*m**2+dz*m**3
797
 
            return mathutils.Vector((x, y, z))
798
 
        
799
 
    # no interpolation needed
800
 
    if segments == 1:
801
 
        for i, line in enumerate(lines):
802
 
            if i < len(lines)-1:
803
 
                faces.append([line[0], lines[i+1][0], lines[i+1][1], line[1]])
804
 
    # more than 1 segment, interpolate
805
 
    else:
806
 
        # calculate splines (if necessary) once, so no recalculations needed
807
 
        if interpolation == 'cubic':
808
 
            splines = []
809
 
            for line in lines:
810
 
                v1 = mesh.vertices[line[0]].co
811
 
                v2 = mesh.vertices[line[1]].co
812
 
                size = (v2-v1).length * cubic_strength
813
 
                splines.append(bridge_calculate_cubic_spline(mesh,
814
 
                    [v1+size*vertex_normals[line[0]], v1, v2,
815
 
                    v2+size*vertex_normals[line[1]]]))
816
 
        else:
817
 
            splines = False
818
 
        
819
 
        # create starting situation
820
 
        virtual_width = [(mesh.vertices[lines[i][0]].co -
821
 
                          mesh.vertices[lines[i+1][0]].co).length for i
822
 
                          in range(len(lines)-1)]
823
 
        new_verts = [get_location(0, seg, splines) for seg in range(1,
824
 
            segments)]
825
 
        first_line_indices = [i for i in range(max_vert_index+1,
826
 
            max_vert_index+segments)]
827
 
        
828
 
        prev_verts = new_verts[:] # vertex locations of verts on previous line
829
 
        prev_vert_indices = first_line_indices[:]
830
 
        max_vert_index += segments - 1 # highest vertex index in virtual mesh
831
 
        next_verts = [] # vertex locations of verts on current line
832
 
        next_vert_indices = []
833
 
        
834
 
        for i, line in enumerate(lines):
835
 
            if i < len(lines)-1:
836
 
                v1 = line[0]
837
 
                v2 = lines[i+1][0]
838
 
                end_face = True
839
 
                for seg in range(1, segments):
840
 
                    loc1 = prev_verts[seg-1]
841
 
                    loc2 = get_location(i+1, seg, splines)
842
 
                    if (loc1-loc2).length < (min_width/100)*virtual_width[i] \
843
 
                    and line[1]==lines[i+1][1]:
844
 
                        # triangle, no new vertex
845
 
                        faces.append([v1, v2, prev_vert_indices[seg-1],
846
 
                            prev_vert_indices[seg-1]])
847
 
                        next_verts += prev_verts[seg-1:]
848
 
                        next_vert_indices += prev_vert_indices[seg-1:]
849
 
                        end_face = False
850
 
                        break
851
 
                    else:
852
 
                        if i == len(lines)-2 and lines[0] == lines[-1]:
853
 
                            # quad with first line, no new vertex
854
 
                            faces.append([v1, v2, first_line_indices[seg-1],
855
 
                                prev_vert_indices[seg-1]])
856
 
                            v2 = first_line_indices[seg-1]
857
 
                            v1 = prev_vert_indices[seg-1]
858
 
                        else:
859
 
                            # quad, add new vertex
860
 
                            max_vert_index += 1
861
 
                            faces.append([v1, v2, max_vert_index,
862
 
                                prev_vert_indices[seg-1]])
863
 
                            v2 = max_vert_index
864
 
                            v1 = prev_vert_indices[seg-1]
865
 
                            new_verts.append(loc2)
866
 
                            next_verts.append(loc2)
867
 
                            next_vert_indices.append(max_vert_index)
868
 
                if end_face:
869
 
                    faces.append([v1, v2, lines[i+1][1], line[1]])
870
 
                
871
 
                prev_verts = next_verts[:]
872
 
                prev_vert_indices = next_vert_indices[:]
873
 
                next_verts = []
874
 
                next_vert_indices = []
875
 
    
876
 
    return(new_verts, faces, max_vert_index)
877
 
 
878
 
 
879
 
# calculate lines (list of lists, vertex indices) that are used for bridging
880
 
def bridge_calculate_lines(mesh, loops, mode, twist, reverse):
881
 
    lines = []
882
 
    loop1, loop2 = [i[0] for i in loops]
883
 
    loop1_circular, loop2_circular = [i[1] for i in loops]
884
 
    circular = loop1_circular or loop2_circular
885
 
    circle_full = False
886
 
    
887
 
    # calculate loop centers
888
 
    centers = []
889
 
    for loop in [loop1, loop2]:
890
 
        center = mathutils.Vector()
891
 
        for vertex in loop:
892
 
            center += mesh.vertices[vertex].co
893
 
        center /= len(loop)
894
 
        centers.append(center)
895
 
    for i, loop in enumerate([loop1, loop2]):
896
 
        for vertex in loop:
897
 
            if mesh.vertices[vertex].co == centers[i]:
898
 
                # prevent zero-length vectors in angle comparisons
899
 
                centers[i] += mathutils.Vector((0.01, 0, 0))
900
 
                break
901
 
    center1, center2 = centers
902
 
    
903
 
    # calculate the normals of the virtual planes that the loops are on
904
 
    normals = []
905
 
    normal_plurity = False
906
 
    for i, loop in enumerate([loop1, loop2]):
907
 
        # covariance matrix
908
 
        mat = mathutils.Matrix(((0.0, 0.0, 0.0),
909
 
                                (0.0, 0.0, 0.0),
910
 
                                (0.0, 0.0, 0.0)))
911
 
        x, y, z = centers[i]
912
 
        for loc in [mesh.vertices[vertex].co for vertex in loop]:
913
 
            mat[0][0] += (loc[0]-x)**2
914
 
            mat[1][0] += (loc[0]-x)*(loc[1]-y)
915
 
            mat[2][0] += (loc[0]-x)*(loc[2]-z)
916
 
            mat[0][1] += (loc[1]-y)*(loc[0]-x)
917
 
            mat[1][1] += (loc[1]-y)**2
918
 
            mat[2][1] += (loc[1]-y)*(loc[2]-z)
919
 
            mat[0][2] += (loc[2]-z)*(loc[0]-x)
920
 
            mat[1][2] += (loc[2]-z)*(loc[1]-y)
921
 
            mat[2][2] += (loc[2]-z)**2
922
 
        # plane normal
923
 
        normal = False
924
 
        if sum(mat[0]) < 1e-6 or sum(mat[1]) < 1e-6 or sum(mat[2]) < 1e-6:
925
 
            normal_plurity = True
926
 
        try:
927
 
            mat.invert()
928
 
        except:
929
 
            if sum(mat[0]) == 0:
930
 
                normal = mathutils.Vector((1.0, 0.0, 0.0))
931
 
            elif sum(mat[1]) == 0:
932
 
                normal = mathutils.Vector((0.0, 1.0, 0.0))
933
 
            elif sum(mat[2]) == 0:
934
 
                normal = mathutils.Vector((0.0, 0.0, 1.0))
935
 
        if not normal:
936
 
            # warning! this is different from .normalize()
937
 
            itermax = 500
938
 
            iter = 0
939
 
            vec = mathutils.Vector((1.0, 1.0, 1.0))
940
 
            vec2 = (mat * vec)/(mat * vec).length
941
 
            while vec != vec2 and iter<itermax:
942
 
                iter+=1
943
 
                vec = vec2
944
 
                vec2 = mat * vec
945
 
                if vec2.length != 0:
946
 
                    vec2 /= vec2.length
947
 
            if vec2.length == 0:
948
 
                vec2 = mathutils.Vector((1.0, 1.0, 1.0))
949
 
            normal = vec2
950
 
        normals.append(normal)
951
 
    # have plane normals face in the same direction (maximum angle: 90 degrees)
952
 
    if ((center1 + normals[0]) - center2).length < \
953
 
    ((center1 - normals[0]) - center2).length:
954
 
        normals[0].negate()
955
 
    if ((center2 + normals[1]) - center1).length > \
956
 
    ((center2 - normals[1]) - center1).length:
957
 
        normals[1].negate()
958
 
    
959
 
    # rotation matrix, representing the difference between the plane normals
960
 
    axis = normals[0].cross(normals[1])
961
 
    axis = mathutils.Vector([loc if abs(loc) > 1e-8 else 0 for loc in axis])
962
 
    if axis.angle(mathutils.Vector((0, 0, 1)), 0) > 1.5707964:
963
 
        axis.negate()
964
 
    angle = normals[0].dot(normals[1])
965
 
    rotation_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
966
 
    
967
 
    # if circular, rotate loops so they are aligned
968
 
    if circular:
969
 
        # make sure loop1 is the circular one (or both are circular)
970
 
        if loop2_circular and not loop1_circular:
971
 
            loop1_circular, loop2_circular = True, False
972
 
            loop1, loop2 = loop2, loop1
973
 
        
974
 
        # match start vertex of loop1 with loop2
975
 
        target_vector = mesh.vertices[loop2[0]].co - center2
976
 
        dif_angles = [[(rotation_matrix * (mesh.vertices[vertex].co - center1)
977
 
                       ).angle(target_vector, 0), False, i] for
978
 
                       i, vertex in enumerate(loop1)]
979
 
        dif_angles.sort()
980
 
        if len(loop1) != len(loop2):
981
 
            angle_limit = dif_angles[0][0] * 1.2 # 20% margin
982
 
            dif_angles = [[(mesh.vertices[loop2[0]].co - \
983
 
                mesh.vertices[loop1[index]].co).length, angle, index] for \
984
 
                angle, distance, index in dif_angles if angle <= angle_limit]
985
 
            dif_angles.sort()
986
 
        loop1 = loop1[dif_angles[0][2]:] + loop1[:dif_angles[0][2]]
987
 
    
988
 
    # have both loops face the same way
989
 
    if normal_plurity and not circular:
990
 
        second_to_first, second_to_second, second_to_last = \
991
 
            [(mesh.vertices[loop1[1]].co - center1).\
992
 
            angle(mesh.vertices[loop2[i]].co - center2) for i in [0, 1, -1]]
993
 
        last_to_first, last_to_second = [(mesh.vertices[loop1[-1]].co - \
994
 
            center1).angle(mesh.vertices[loop2[i]].co - center2) for \
995
 
            i in [0, 1]]
996
 
        if (min(last_to_first, last_to_second)*1.1 < min(second_to_first, \
997
 
        second_to_second)) or (loop2_circular and second_to_last*1.1 < \
998
 
        min(second_to_first, second_to_second)):
999
 
            loop1.reverse()
1000
 
            if circular:
1001
 
                loop1 = [loop1[-1]] + loop1[:-1]
1002
 
    else:
1003
 
        angle = (mesh.vertices[loop1[0]].co - center1).\
1004
 
            cross(mesh.vertices[loop1[1]].co - center1).angle(normals[0], 0)
1005
 
        target_angle = (mesh.vertices[loop2[0]].co - center2).\
1006
 
            cross(mesh.vertices[loop2[1]].co - center2).angle(normals[1], 0)
1007
 
        limit = 1.5707964 # 0.5*pi, 90 degrees
1008
 
        if not ((angle > limit and target_angle > limit) or \
1009
 
        (angle < limit and target_angle < limit)):
1010
 
            loop1.reverse()
1011
 
            if circular:
1012
 
                loop1 = [loop1[-1]] + loop1[:-1]
1013
 
        elif normals[0].angle(normals[1]) > limit:
1014
 
            loop1.reverse()
1015
 
            if circular:
1016
 
                loop1 = [loop1[-1]] + loop1[:-1]
1017
 
    
1018
 
    # both loops have the same length
1019
 
    if len(loop1) == len(loop2):
1020
 
        # manual override
1021
 
        if twist:
1022
 
            if abs(twist) < len(loop1):
1023
 
                loop1 = loop1[twist:]+loop1[:twist]
1024
 
        if reverse:
1025
 
            loop1.reverse()
1026
 
        
1027
 
        lines.append([loop1[0], loop2[0]])
1028
 
        for i in range(1, len(loop1)):
1029
 
            lines.append([loop1[i], loop2[i]])
1030
 
    
1031
 
    # loops of different lengths
1032
 
    else:
1033
 
        # make loop1 longest loop
1034
 
        if len(loop2) > len(loop1):
1035
 
            loop1, loop2 = loop2, loop1
1036
 
            loop1_circular, loop2_circular = loop2_circular, loop1_circular
1037
 
        
1038
 
        # manual override
1039
 
        if twist:
1040
 
            if abs(twist) < len(loop1):
1041
 
                loop1 = loop1[twist:]+loop1[:twist]
1042
 
        if reverse:
1043
 
            loop1.reverse()
1044
 
            
1045
 
        # shortest angle difference doesn't always give correct start vertex
1046
 
        if loop1_circular and not loop2_circular:
1047
 
            shifting = 1
1048
 
            while shifting:
1049
 
                if len(loop1) - shifting < len(loop2):
1050
 
                    shifting = False
1051
 
                    break
1052
 
                to_last, to_first = [(rotation_matrix *
1053
 
                    (mesh.vertices[loop1[-1]].co - center1)).angle((mesh.\
1054
 
                    vertices[loop2[i]].co - center2), 0) for i in [-1, 0]]
1055
 
                if to_first < to_last:
1056
 
                    loop1 = [loop1[-1]] + loop1[:-1]
1057
 
                    shifting += 1
1058
 
                else:
1059
 
                    shifting = False
1060
 
                    break
1061
 
        
1062
 
        # basic shortest side first
1063
 
        if mode == 'basic':
1064
 
            lines.append([loop1[0], loop2[0]])
1065
 
            for i in range(1, len(loop1)):
1066
 
                if i >= len(loop2) - 1:
1067
 
                    # triangles
1068
 
                    lines.append([loop1[i], loop2[-1]])
1069
 
                else:
1070
 
                    # quads
1071
 
                    lines.append([loop1[i], loop2[i]])
1072
 
        
1073
 
        # shortest edge algorithm
1074
 
        else: # mode == 'shortest'
1075
 
            lines.append([loop1[0], loop2[0]])
1076
 
            prev_vert2 = 0
1077
 
            for i in range(len(loop1) -1):
1078
 
                if prev_vert2 == len(loop2) - 1 and not loop2_circular:
1079
 
                    # force triangles, reached end of loop2
1080
 
                    tri, quad = 0, 1
1081
 
                elif prev_vert2 == len(loop2) - 1 and loop2_circular:
1082
 
                    # at end of loop2, but circular, so check with first vert
1083
 
                    tri, quad = [(mesh.vertices[loop1[i+1]].co -
1084
 
                                  mesh.vertices[loop2[j]].co).length
1085
 
                                 for j in [prev_vert2, 0]]
1086
 
 
1087
 
                    circle_full = 2
1088
 
                elif len(loop1) - 1 - i == len(loop2) - 1 - prev_vert2 and \
1089
 
                not circle_full:
1090
 
                    # force quads, otherwise won't make it to end of loop2
1091
 
                    tri, quad = 1, 0
1092
 
                else:
1093
 
                    # calculate if tri or quad gives shortest edge
1094
 
                    tri, quad = [(mesh.vertices[loop1[i+1]].co -
1095
 
                                  mesh.vertices[loop2[j]].co).length
1096
 
                                 for j in range(prev_vert2, prev_vert2+2)]
1097
 
                
1098
 
                # triangle
1099
 
                if tri < quad:
1100
 
                    lines.append([loop1[i+1], loop2[prev_vert2]])
1101
 
                    if circle_full == 2:
1102
 
                        circle_full = False
1103
 
                # quad
1104
 
                elif not circle_full:
1105
 
                    lines.append([loop1[i+1], loop2[prev_vert2+1]])
1106
 
                    prev_vert2 += 1
1107
 
                # quad to first vertex of loop2
1108
 
                else:
1109
 
                    lines.append([loop1[i+1], loop2[0]])
1110
 
                    prev_vert2 = 0
1111
 
                    circle_full = True
1112
 
    
1113
 
    # final face for circular loops
1114
 
    if loop1_circular and loop2_circular:
1115
 
        lines.append([loop1[0], loop2[0]])
1116
 
    
1117
 
    return(lines)
1118
 
 
1119
 
 
1120
 
# calculate number of segments needed
1121
 
def bridge_calculate_segments(mesh, lines, loops, segments):
1122
 
    # return if amount of segments is set by user
1123
 
    if segments != 0:
1124
 
        return segments
1125
 
    
1126
 
    # edge lengths
1127
 
    average_edge_length = [(mesh.vertices[vertex].co - \
1128
 
        mesh.vertices[loop[0][i+1]].co).length for loop in loops for \
1129
 
        i, vertex in enumerate(loop[0][:-1])]
1130
 
    # closing edges of circular loops
1131
 
    average_edge_length += [(mesh.vertices[loop[0][-1]].co - \
1132
 
        mesh.vertices[loop[0][0]].co).length for loop in loops if loop[1]] 
1133
 
    
1134
 
    # average lengths
1135
 
    average_edge_length = sum(average_edge_length) / len(average_edge_length)
1136
 
    average_bridge_length = sum([(mesh.vertices[v1].co - \
1137
 
        mesh.vertices[v2].co).length for v1, v2 in lines]) / len(lines)
1138
 
    
1139
 
    segments = max(1, round(average_bridge_length / average_edge_length))
1140
 
        
1141
 
    return(segments)
1142
 
 
1143
 
 
1144
 
# return dictionary with vertex index as key, and the normal vector as value
1145
 
def bridge_calculate_virtual_vertex_normals(mesh, lines, loops, edge_faces,
1146
 
edgekey_to_edge):
1147
 
    if not edge_faces: # interpolation isn't set to cubic
1148
 
        return False
1149
 
    
1150
 
    # pity reduce() isn't one of the basic functions in python anymore
1151
 
    def average_vector_dictionary(dic):
1152
 
        for key, vectors in dic.items():
1153
 
            #if type(vectors) == type([]) and len(vectors) > 1:
1154
 
            if len(vectors) > 1:
1155
 
                average = mathutils.Vector()
1156
 
                for vector in vectors:
1157
 
                    average += vector
1158
 
                average /= len(vectors)
1159
 
                dic[key] = [average]
1160
 
        return dic
1161
 
    
1162
 
    # get all edges of the loop
1163
 
    edges = [[edgekey_to_edge[tuple(sorted([loops[j][0][i],
1164
 
        loops[j][0][i+1]]))] for i in range(len(loops[j][0])-1)] for \
1165
 
        j in [0,1]]
1166
 
    edges = edges[0] + edges[1]
1167
 
    for j in [0, 1]:
1168
 
        if loops[j][1]: # circular
1169
 
            edges.append(edgekey_to_edge[tuple(sorted([loops[j][0][0],
1170
 
                loops[j][0][-1]]))])
1171
 
    
1172
 
    """
1173
 
    calculation based on face topology (assign edge-normals to vertices)
1174
 
    
1175
 
    edge_normal = face_normal x edge_vector
1176
 
    vertex_normal = average(edge_normals)
1177
 
    """
1178
 
    vertex_normals = dict([(vertex, []) for vertex in loops[0][0]+loops[1][0]])
1179
 
    for edge in edges:
1180
 
        faces = edge_faces[edge.key] # valid faces connected to edge
1181
 
        
1182
 
        if faces:
1183
 
            # get edge coordinates
1184
 
            v1, v2 = [mesh.vertices[edge.key[i]].co for i in [0,1]]
1185
 
            edge_vector = v1 - v2
1186
 
            if edge_vector.length < 1e-4:
1187
 
                # zero-length edge, vertices at same location
1188
 
                continue
1189
 
            edge_center = (v1 + v2) / 2
1190
 
            
1191
 
            # average face coordinates, if connected to more than 1 valid face
1192
 
            if len(faces) > 1:
1193
 
                face_normal = mathutils.Vector()
1194
 
                face_center = mathutils.Vector()
1195
 
                for face in faces:
1196
 
                    face_normal += face.normal
1197
 
                    face_center += face.center
1198
 
                face_normal /= len(faces)
1199
 
                face_center /= len(faces)
1200
 
            else:
1201
 
                face_normal = faces[0].normal
1202
 
                face_center = faces[0].center
1203
 
            if face_normal.length < 1e-4:
1204
 
                # faces with a surface of 0 have no face normal
1205
 
                continue
1206
 
            
1207
 
            # calculate virtual edge normal
1208
 
            edge_normal = edge_vector.cross(face_normal)
1209
 
            edge_normal.length = 0.01
1210
 
            if (face_center - (edge_center + edge_normal)).length > \
1211
 
            (face_center - (edge_center - edge_normal)).length:
1212
 
                # make normal face the correct way
1213
 
                edge_normal.negate()
1214
 
            edge_normal.normalize()
1215
 
            # add virtual edge normal as entry for both vertices it connects
1216
 
            for vertex in edge.key:
1217
 
                vertex_normals[vertex].append(edge_normal)
1218
 
    
1219
 
    """ 
1220
 
    calculation based on connection with other loop (vertex focused method) 
1221
 
    - used for vertices that aren't connected to any valid faces
1222
 
    
1223
 
    plane_normal = edge_vector x connection_vector
1224
 
    vertex_normal = plane_normal x edge_vector
1225
 
    """
1226
 
    vertices = [vertex for vertex, normal in vertex_normals.items() if not \
1227
 
        normal]
1228
 
    
1229
 
    if vertices:
1230
 
        # edge vectors connected to vertices
1231
 
        edge_vectors = dict([[vertex, []] for vertex in vertices])
1232
 
        for edge in edges:
1233
 
            for v in edge.key:
1234
 
                if v in edge_vectors:
1235
 
                    edge_vector = mesh.vertices[edge.key[0]].co - \
1236
 
                        mesh.vertices[edge.key[1]].co
1237
 
                    if edge_vector.length < 1e-4:
1238
 
                        # zero-length edge, vertices at same location
1239
 
                        continue
1240
 
                    edge_vectors[v].append(edge_vector)
1241
 
    
1242
 
        # connection vectors between vertices of both loops
1243
 
        connection_vectors = dict([[vertex, []] for vertex in vertices])
1244
 
        connections = dict([[vertex, []] for vertex in vertices])
1245
 
        for v1, v2 in lines:
1246
 
            if v1 in connection_vectors or v2 in connection_vectors:
1247
 
                new_vector = mesh.vertices[v1].co - mesh.vertices[v2].co
1248
 
                if new_vector.length < 1e-4:
1249
 
                    # zero-length connection vector,
1250
 
                    # vertices in different loops at same location
1251
 
                    continue
1252
 
                if v1 in connection_vectors:
1253
 
                    connection_vectors[v1].append(new_vector)
1254
 
                    connections[v1].append(v2)
1255
 
                if v2 in connection_vectors:
1256
 
                    connection_vectors[v2].append(new_vector)
1257
 
                    connections[v2].append(v1)
1258
 
        connection_vectors = average_vector_dictionary(connection_vectors)
1259
 
        connection_vectors = dict([[vertex, vector[0]] if vector else \
1260
 
            [vertex, []] for vertex, vector in connection_vectors.items()])
1261
 
        
1262
 
        for vertex, values in edge_vectors.items():
1263
 
            # vertex normal doesn't matter, just assign a random vector to it
1264
 
            if not connection_vectors[vertex]:
1265
 
                vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1266
 
                continue
1267
 
            
1268
 
            # calculate to what location the vertex is connected, 
1269
 
            # used to determine what way to flip the normal
1270
 
            connected_center = mathutils.Vector()
1271
 
            for v in connections[vertex]:
1272
 
                connected_center += mesh.vertices[v].co
1273
 
            if len(connections[vertex]) > 1:
1274
 
                connected_center /= len(connections[vertex])
1275
 
            if len(connections[vertex]) == 0:
1276
 
                # shouldn't be possible, but better safe than sorry
1277
 
                vertex_normals[vertex] = [mathutils.Vector((1, 0, 0))]
1278
 
                continue
1279
 
            
1280
 
            # can't do proper calculations, because of zero-length vector
1281
 
            if not values:
1282
 
                if (connected_center - (mesh.vertices[vertex].co + \
1283
 
                connection_vectors[vertex])).length < (connected_center - \
1284
 
                (mesh.vertices[vertex].co - connection_vectors[vertex])).\
1285
 
                length:
1286
 
                    connection_vectors[vertex].negate()
1287
 
                vertex_normals[vertex] = [connection_vectors[vertex].\
1288
 
                    normalized()]
1289
 
                continue
1290
 
            
1291
 
            # calculate vertex normals using edge-vectors,
1292
 
            # connection-vectors and the derived plane normal
1293
 
            for edge_vector in values:
1294
 
                plane_normal = edge_vector.cross(connection_vectors[vertex])
1295
 
                vertex_normal = edge_vector.cross(plane_normal)
1296
 
                vertex_normal.length = 0.1
1297
 
                if (connected_center - (mesh.vertices[vertex].co + \
1298
 
                vertex_normal)).length < (connected_center - \
1299
 
                (mesh.vertices[vertex].co - vertex_normal)).length:
1300
 
                # make normal face the correct way
1301
 
                    vertex_normal.negate()
1302
 
                vertex_normal.normalize()
1303
 
                vertex_normals[vertex].append(vertex_normal)
1304
 
    
1305
 
    # average virtual vertex normals, based on all edges it's connected to
1306
 
    vertex_normals = average_vector_dictionary(vertex_normals)
1307
 
    vertex_normals = dict([[vertex, vector[0]] for vertex, vector in \
1308
 
        vertex_normals.items()])
1309
 
    
1310
 
    return(vertex_normals)
1311
 
 
1312
 
 
1313
 
# add vertices to mesh
1314
 
def bridge_create_vertices(mesh, vertices):
1315
 
    start_index = len(mesh.vertices)
1316
 
    mesh.vertices.add(len(vertices))
1317
 
    for i in range(len(vertices)):
1318
 
        mesh.vertices[start_index + i].co = vertices[i]
1319
 
 
1320
 
 
1321
 
# add faces to mesh
1322
 
def bridge_create_faces(mesh, faces, twist):
1323
 
    # have the normal point the correct way
1324
 
    if twist < 0:
1325
 
        [face.reverse() for face in faces]
1326
 
        faces = [face[2:]+face[:2] if face[0]==face[1] else face for \
1327
 
            face in faces]
1328
 
    
1329
 
    # eekadoodle prevention
1330
 
    for i in range(len(faces)):
1331
 
        if not faces[i][-1]:
1332
 
            if faces[i][0] == faces[i][-1]:
1333
 
                faces[i] = [faces[i][1], faces[i][2], faces[i][3], faces[i][1]]
1334
 
            else:
1335
 
                faces[i] = [faces[i][-1]] + faces[i][:-1]
1336
 
    
1337
 
    start_faces = len(mesh.tessfaces)
1338
 
    mesh.tessfaces.add(len(faces))
1339
 
    for i in range(len(faces)):
1340
 
        mesh.tessfaces[start_faces + i].vertices_raw = faces[i]
1341
 
    mesh.update(calc_edges = True) # calc_edges prevents memory-corruption
1342
 
 
1343
 
 
1344
 
# calculate input loops
1345
 
def bridge_get_input(mesh):
1346
 
    # create list of internal edges, which should be skipped
1347
 
    eks_of_selected_faces = [item for sublist in [face.edge_keys for face \
1348
 
        in mesh.tessfaces if face.select and not face.hide] for item in sublist]
1349
 
    edge_count = {}
1350
 
    for ek in eks_of_selected_faces:
1351
 
        if ek in edge_count:
1352
 
            edge_count[ek] += 1
1353
 
        else:
1354
 
            edge_count[ek] = 1
1355
 
    internal_edges = [ek for ek in edge_count if edge_count[ek] > 1]
1356
 
    
1357
 
    # sort correct edges into loops
1358
 
    selected_edges = [edge.key for edge in mesh.edges if edge.select \
1359
 
        and not edge.hide and edge.key not in internal_edges]
1360
 
    loops = get_connected_selections(selected_edges)
1361
 
    
1362
 
    return(loops)
1363
 
 
1364
 
 
1365
 
# return values needed by the bridge operator
1366
 
def bridge_initialise(mesh, interpolation):
1367
 
    if interpolation == 'cubic':
1368
 
        # dict with edge-key as key and list of connected valid faces as value
1369
 
        face_blacklist = [face.index for face in mesh.tessfaces if face.select or \
1370
 
            face.hide]
1371
 
        edge_faces = dict([[edge.key, []] for edge in mesh.edges if not \
1372
 
            edge.hide])
1373
 
        for face in mesh.tessfaces:
1374
 
            if face.index in face_blacklist:
1375
 
                continue
1376
 
            for key in face.edge_keys:
1377
 
                edge_faces[key].append(face)
1378
 
        # dictionary with the edge-key as key and edge as value
1379
 
        edgekey_to_edge = dict([[edge.key, edge] for edge in mesh.edges if \
1380
 
            edge.select and not edge.hide])
1381
 
    else:
1382
 
        edge_faces = False
1383
 
        edgekey_to_edge = False
1384
 
    
1385
 
    # selected faces input
1386
 
    old_selected_faces = [face.index for face in mesh.tessfaces if face.select \
1387
 
        and not face.hide]
1388
 
    
1389
 
    # find out if faces created by bridging should be smoothed
1390
 
    smooth = False
1391
 
    if mesh.tessfaces:
1392
 
        if sum([face.use_smooth for face in mesh.tessfaces])/len(mesh.tessfaces) \
1393
 
        >= 0.5:
1394
 
            smooth = True
1395
 
    
1396
 
    return(edge_faces, edgekey_to_edge, old_selected_faces, smooth)
1397
 
 
1398
 
 
1399
 
# return a string with the input method
1400
 
def bridge_input_method(loft, loft_loop):
1401
 
    method = ""
1402
 
    if loft:
1403
 
        if loft_loop:
1404
 
            method = "Loft loop"
1405
 
        else:
1406
 
            method = "Loft no-loop"
1407
 
    else:
1408
 
        method = "Bridge"
1409
 
    
1410
 
    return(method)
1411
 
 
1412
 
 
1413
 
# match up loops in pairs, used for multi-input bridging
1414
 
def bridge_match_loops(mesh, loops):
1415
 
    # calculate average loop normals and centers
1416
 
    normals = []
1417
 
    centers = []
1418
 
    for vertices, circular in loops:
1419
 
        normal = mathutils.Vector()
1420
 
        center = mathutils.Vector()
1421
 
        for vertex in vertices:
1422
 
            normal += mesh.vertices[vertex].normal
1423
 
            center += mesh.vertices[vertex].co
1424
 
        normals.append(normal / len(vertices) / 10)
1425
 
        centers.append(center / len(vertices))
1426
 
    
1427
 
    # possible matches if loop normals are faced towards the center
1428
 
    # of the other loop
1429
 
    matches = dict([[i, []] for i in range(len(loops))])
1430
 
    matches_amount = 0
1431
 
    for i in range(len(loops) + 1):
1432
 
        for j in range(i+1, len(loops)):
1433
 
            if (centers[i] - centers[j]).length > (centers[i] - (centers[j] \
1434
 
            + normals[j])).length and (centers[j] - centers[i]).length > \
1435
 
            (centers[j] - (centers[i] + normals[i])).length:
1436
 
                matches_amount += 1
1437
 
                matches[i].append([(centers[i] - centers[j]).length, i, j])
1438
 
                matches[j].append([(centers[i] - centers[j]).length, j, i])
1439
 
    # if no loops face each other, just make matches between all the loops
1440
 
    if matches_amount == 0:
1441
 
        for i in range(len(loops) + 1):
1442
 
            for j in range(i+1, len(loops)):
1443
 
                matches[i].append([(centers[i] - centers[j]).length, i, j])
1444
 
                matches[j].append([(centers[i] - centers[j]).length, j, i])
1445
 
    for key, value in matches.items():
1446
 
        value.sort()
1447
 
    
1448
 
    # matches based on distance between centers and number of vertices in loops
1449
 
    new_order = []
1450
 
    for loop_index in range(len(loops)):
1451
 
        if loop_index in new_order:
1452
 
            continue
1453
 
        loop_matches = matches[loop_index]
1454
 
        if not loop_matches:
1455
 
            continue
1456
 
        shortest_distance = loop_matches[0][0]
1457
 
        shortest_distance *= 1.1
1458
 
        loop_matches = [[abs(len(loops[loop_index][0]) - \
1459
 
            len(loops[loop[2]][0])), loop[0], loop[1], loop[2]] for loop in \
1460
 
            loop_matches if loop[0] < shortest_distance]
1461
 
        loop_matches.sort()
1462
 
        for match in loop_matches:
1463
 
            if match[3] not in new_order:
1464
 
                new_order += [loop_index, match[3]]
1465
 
                break
1466
 
    
1467
 
    # reorder loops based on matches
1468
 
    if len(new_order) >= 2:
1469
 
        loops = [loops[i] for i in new_order]
1470
 
    
1471
 
    return(loops)
1472
 
 
1473
 
 
1474
 
# have normals of selection face outside
1475
 
def bridge_recalculate_normals():
1476
 
    bpy.ops.object.mode_set(mode = 'EDIT')
1477
 
    bpy.ops.mesh.normals_make_consistent()
1478
 
 
1479
 
 
1480
 
# remove old_selected_faces
1481
 
def bridge_remove_internal_faces(mesh, old_selected_faces):
1482
 
    select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode]
1483
 
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]
1484
 
    
1485
 
    # hack to keep track of the current selection
1486
 
    for edge in mesh.edges:
1487
 
        if edge.select and not edge.hide:
1488
 
            edge.bevel_weight = (edge.bevel_weight/3) + 0.2
1489
 
        else:
1490
 
            edge.bevel_weight = (edge.bevel_weight/3) + 0.6
1491
 
    
1492
 
    # remove faces
1493
 
    bpy.ops.object.mode_set(mode = 'EDIT')
1494
 
    bpy.ops.mesh.select_all(action = 'DESELECT')
1495
 
    bpy.ops.object.mode_set(mode = 'OBJECT')
1496
 
    for face in old_selected_faces:
1497
 
        mesh.tessfaces[face].select = True
1498
 
    bpy.ops.object.mode_set(mode = 'EDIT')
1499
 
    bpy.ops.mesh.delete(type = 'FACE')
1500
 
    
1501
 
    # restore old selection, using hack
1502
 
    bpy.ops.object.mode_set(mode = 'OBJECT')
1503
 
    bpy.context.tool_settings.mesh_select_mode = [False, True, False]
1504
 
    for edge in mesh.edges:
1505
 
        if edge.bevel_weight < 0.6:
1506
 
            edge.bevel_weight = (edge.bevel_weight-0.2) * 3
1507
 
            edge.select = True
1508
 
        else:
1509
 
            edge.bevel_weight = (edge.bevel_weight-0.6) * 3
1510
 
    bpy.ops.object.mode_set(mode = 'EDIT')
1511
 
    bpy.ops.object.mode_set(mode = 'OBJECT')
1512
 
    bpy.context.tool_settings.mesh_select_mode = select_mode
1513
 
 
1514
 
 
1515
 
# update list of internal faces that are flagged for removal
1516
 
def bridge_save_unused_faces(mesh, old_selected_faces, loops):
1517
 
    # key: vertex index, value: lists of selected faces using it
1518
 
    vertex_to_face = dict([[i, []] for i in range(len(mesh.vertices))])
1519
 
    [[vertex_to_face[vertex_index].append(face) for vertex_index in \
1520
 
        mesh.tessfaces[face].vertices] for face in old_selected_faces]
1521
 
    
1522
 
    # group selected faces that are connected
1523
 
    groups = []
1524
 
    grouped_faces = []
1525
 
    for face in old_selected_faces:
1526
 
        if face in grouped_faces:
1527
 
            continue
1528
 
        grouped_faces.append(face)
1529
 
        group = [face]
1530
 
        new_faces = [face]
1531
 
        while new_faces:
1532
 
            grow_face = new_faces[0]
1533
 
            for vertex in mesh.tessfaces[grow_face].vertices:
1534
 
                vertex_face_group = [face for face in vertex_to_face[vertex] \
1535
 
                    if face not in grouped_faces]
1536
 
                new_faces += vertex_face_group
1537
 
                grouped_faces += vertex_face_group
1538
 
                group += vertex_face_group
1539
 
            new_faces.pop(0)
1540
 
        groups.append(group)
1541
 
    
1542
 
    # key: vertex index, value: True/False (is it in a loop that is used)
1543
 
    used_vertices = dict([[i, 0] for i in range(len(mesh.vertices))])
1544
 
    for loop in loops:
1545
 
        for vertex in loop[0]:
1546
 
            used_vertices[vertex] = True
1547
 
    
1548
 
    # check if group is bridged, if not remove faces from internal faces list
1549
 
    for group in groups:
1550
 
        used = False
1551
 
        for face in group:
1552
 
            if used:
1553
 
                break
1554
 
            for vertex in mesh.tessfaces[face].vertices:
1555
 
                if used_vertices[vertex]:
1556
 
                    used = True
1557
 
                    break
1558
 
        if not used:
1559
 
            for face in group:
1560
 
                old_selected_faces.remove(face)
1561
 
 
1562
 
 
1563
 
# add the newly created faces to the selection
1564
 
def bridge_select_new_faces(mesh, amount, smooth):
1565
 
    select_mode = [i for i in bpy.context.tool_settings.mesh_select_mode]
1566
 
    bpy.context.tool_settings.mesh_select_mode = [False, False, True]
1567
 
    for i in range(amount):
1568
 
        mesh.tessfaces[-(i+1)].select = True
1569
 
        mesh.tessfaces[-(i+1)].use_smooth = smooth
1570
 
    bpy.ops.object.mode_set(mode = 'EDIT')
1571
 
    bpy.ops.object.mode_set(mode = 'OBJECT')
1572
 
    bpy.context.tool_settings.mesh_select_mode = select_mode
1573
 
 
1574
 
 
1575
 
# sort loops, so they are connected in the correct order when lofting
1576
 
def bridge_sort_loops(mesh, loops, loft_loop):
1577
 
    # simplify loops to single points, and prepare for pathfinding
1578
 
    x, y, z = [[sum([mesh.vertices[i].co[j] for i in loop[0]]) / \
1579
 
        len(loop[0]) for loop in loops] for j in range(3)]
1580
 
    nodes = [mathutils.Vector((x[i], y[i], z[i])) for i in range(len(loops))]
1581
 
    
1582
 
    active_node = 0
1583
 
    open = [i for i in range(1, len(loops))]
1584
 
    path = [[0,0]]
1585
 
    # connect node to path, that is shortest to active_node
1586
 
    while len(open) > 0:
1587
 
        distances = [(nodes[active_node] - nodes[i]).length for i in open]
1588
 
        active_node = open[distances.index(min(distances))]
1589
 
        open.remove(active_node)
1590
 
        path.append([active_node, min(distances)])
1591
 
    # check if we didn't start in the middle of the path
1592
 
    for i in range(2, len(path)):
1593
 
        if (nodes[path[i][0]]-nodes[0]).length < path[i][1]:
1594
 
            temp = path[:i]
1595
 
            path.reverse()
1596
 
            path = path[:-i] + temp
1597
 
            break
1598
 
    
1599
 
    # reorder loops
1600
 
    loops = [loops[i[0]] for i in path]
1601
 
    # if requested, duplicate first loop at last position, so loft can loop
1602
 
    if loft_loop:
1603
 
        loops = loops + [loops[0]]
1604
 
    
1605
 
    return(loops)
1606
 
 
1607
 
 
1608
 
##########################################
1609
 
####### Circle functions #################
1610
 
##########################################
1611
 
 
1612
 
# convert 3d coordinates to 2d coordinates on plane
1613
 
def circle_3d_to_2d(mesh_mod, loop, com, normal):
1614
 
    # project vertices onto the plane
1615
 
    verts = [mesh_mod.vertices[v] for v in loop[0]]
1616
 
    verts_projected = [[v.co - (v.co - com).dot(normal) * normal, v.index]
1617
 
                       for v in verts]
1618
 
 
1619
 
    # calculate two vectors (p and q) along the plane
1620
 
    m = mathutils.Vector((normal[0] + 1.0, normal[1], normal[2]))
1621
 
    p = m - (m.dot(normal) * normal)
1622
 
    if p.dot(p) == 0.0:
1623
 
        m = mathutils.Vector((normal[0], normal[1] + 1.0, normal[2]))
1624
 
        p = m - (m.dot(normal) * normal)
1625
 
    q = p.cross(normal)
1626
 
    
1627
 
    # change to 2d coordinates using perpendicular projection
1628
 
    locs_2d = []
1629
 
    for loc, vert in verts_projected:
1630
 
        vloc = loc - com
1631
 
        x = p.dot(vloc) / p.dot(p)
1632
 
        y = q.dot(vloc) / q.dot(q)
1633
 
        locs_2d.append([x, y, vert])
1634
 
    
1635
 
    return(locs_2d, p, q)
1636
 
 
1637
 
 
1638
 
# calculate a best-fit circle to the 2d locations on the plane
1639
 
def circle_calculate_best_fit(locs_2d):
1640
 
    # initial guess
1641
 
    x0 = 0.0
1642
 
    y0 = 0.0
1643
 
    r = 1.0
1644
 
    
1645
 
    # calculate center and radius (non-linear least squares solution)
1646
 
    for iter in range(500):
1647
 
        jmat = []
1648
 
        k = []
1649
 
        for v in locs_2d:
1650
 
            d = (v[0]**2-2.0*x0*v[0]+v[1]**2-2.0*y0*v[1]+x0**2+y0**2)**0.5
1651
 
            jmat.append([(x0-v[0])/d, (y0-v[1])/d, -1.0])
1652
 
            k.append(-(((v[0]-x0)**2+(v[1]-y0)**2)**0.5-r))
1653
 
        jmat2 = mathutils.Matrix(((0.0, 0.0, 0.0),
1654
 
                                  (0.0, 0.0, 0.0),
1655
 
                                  (0.0, 0.0, 0.0),
1656
 
                                  ))
1657
 
        k2 = mathutils.Vector((0.0, 0.0, 0.0))
1658
 
        for i in range(len(jmat)):
1659
 
            k2 += mathutils.Vector(jmat[i])*k[i]
1660
 
            jmat2[0][0] += jmat[i][0]**2
1661
 
            jmat2[1][0] += jmat[i][0]*jmat[i][1]
1662
 
            jmat2[2][0] += jmat[i][0]*jmat[i][2]
1663
 
            jmat2[1][1] += jmat[i][1]**2
1664
 
            jmat2[2][1] += jmat[i][1]*jmat[i][2]
1665
 
            jmat2[2][2] += jmat[i][2]**2
1666
 
        jmat2[0][1] = jmat2[1][0]
1667
 
        jmat2[0][2] = jmat2[2][0]
1668
 
        jmat2[1][2] = jmat2[2][1]
1669
 
        try:
1670
 
            jmat2.invert()
1671
 
        except:
1672
 
            pass
1673
 
        dx0, dy0, dr = jmat2 * k2
1674
 
        x0 += dx0
1675
 
        y0 += dy0
1676
 
        r += dr
1677
 
        # stop iterating if we're close enough to optimal solution
1678
 
        if abs(dx0)<1e-6 and abs(dy0)<1e-6 and abs(dr)<1e-6:
1679
 
            break
1680
 
    
1681
 
    # return center of circle and radius
1682
 
    return(x0, y0, r)
1683
 
 
1684
 
 
1685
 
# calculate circle so no vertices have to be moved away from the center
1686
 
def circle_calculate_min_fit(locs_2d):
1687
 
    # center of circle
1688
 
    x0 = (min([i[0] for i in locs_2d])+max([i[0] for i in locs_2d]))/2.0
1689
 
    y0 = (min([i[1] for i in locs_2d])+max([i[1] for i in locs_2d]))/2.0
1690
 
    center = mathutils.Vector([x0, y0])
1691
 
    # radius of circle
1692
 
    r = min([(mathutils.Vector([i[0], i[1]])-center).length for i in locs_2d])
1693
 
    
1694
 
    # return center of circle and radius
1695
 
    return(x0, y0, r)
1696
 
 
1697
 
 
1698
 
# calculate the new locations of the vertices that need to be moved
1699
 
def circle_calculate_verts(flatten, mesh_mod, locs_2d, com, p, q, normal):
1700
 
    # changing 2d coordinates back to 3d coordinates
1701
 
    locs_3d = []
1702
 
    for loc in locs_2d:
1703
 
        locs_3d.append([loc[2], loc[0]*p + loc[1]*q + com])
1704
 
    
1705
 
    if flatten: # flat circle
1706
 
        return(locs_3d)
1707
 
    
1708
 
    else: # project the locations on the existing mesh
1709
 
        vert_edges = dict_vert_edges(mesh_mod)
1710
 
        vert_faces = dict_vert_faces(mesh_mod)
1711
 
        faces = [f for f in mesh_mod.tessfaces if not f.hide]
1712
 
        rays = [normal, -normal]
1713
 
        new_locs = []
1714
 
        for loc in locs_3d:
1715
 
            projection = False
1716
 
            if mesh_mod.vertices[loc[0]].co == loc[1]: # vertex hasn't moved
1717
 
                projection = loc[1]
1718
 
            else:
1719
 
                dif = normal.angle(loc[1]-mesh_mod.vertices[loc[0]].co)
1720
 
                if -1e-6 < dif < 1e-6 or math.pi-1e-6 < dif < math.pi+1e-6:
1721
 
                    # original location is already along projection normal
1722
 
                    projection = mesh_mod.vertices[loc[0]].co
1723
 
                else:
1724
 
                    # quick search through adjacent faces
1725
 
                    for face in vert_faces[loc[0]]:
1726
 
                        verts = [mesh_mod.vertices[v].co for v in \
1727
 
                            mesh_mod.tessfaces[face].vertices]
1728
 
                        if len(verts) == 3: # triangle
1729
 
                            v1, v2, v3 = verts
1730
 
                            v4 = False
1731
 
                        else: # quad
1732
 
                            v1, v2, v3, v4 = verts
1733
 
                        for ray in rays:
1734
 
                            intersect = mathutils.geometry.\
1735
 
                            intersect_ray_tri(v1, v2, v3, ray, loc[1])
1736
 
                            if intersect:
1737
 
                                projection = intersect
1738
 
                                break
1739
 
                            elif v4:
1740
 
                                intersect = mathutils.geometry.\
1741
 
                                intersect_ray_tri(v1, v3, v4, ray, loc[1])
1742
 
                                if intersect:
1743
 
                                    projection = intersect
1744
 
                                    break
1745
 
                        if projection:
1746
 
                            break
1747
 
            if not projection:
1748
 
                # check if projection is on adjacent edges
1749
 
                for edgekey in vert_edges[loc[0]]:
1750
 
                    line1 = mesh_mod.vertices[edgekey[0]].co
1751
 
                    line2 = mesh_mod.vertices[edgekey[1]].co
1752
 
                    intersect, dist = mathutils.geometry.intersect_point_line(\
1753
 
                        loc[1], line1, line2)
1754
 
                    if 1e-6 < dist < 1 - 1e-6:
1755
 
                        projection = intersect
1756
 
                        break
1757
 
            if not projection:
1758
 
                # full search through the entire mesh
1759
 
                hits = []
1760
 
                for face in faces:
1761
 
                    verts = [mesh_mod.vertices[v].co for v in face.vertices]
1762
 
                    if len(verts) == 3: # triangle
1763
 
                        v1, v2, v3 = verts
1764
 
                        v4 = False
1765
 
                    else: # quad
1766
 
                        v1, v2, v3, v4 = verts
1767
 
                    for ray in rays:
1768
 
                        intersect = mathutils.geometry.intersect_ray_tri(\
1769
 
                            v1, v2, v3, ray, loc[1])
1770
 
                        if intersect:
1771
 
                            hits.append([(loc[1] - intersect).length,
1772
 
                                intersect])
1773
 
                            break
1774
 
                        elif v4:
1775
 
                            intersect = mathutils.geometry.intersect_ray_tri(\
1776
 
                                v1, v3, v4, ray, loc[1])
1777
 
                            if intersect:
1778
 
                                hits.append([(loc[1] - intersect).length,
1779
 
                                    intersect])
1780
 
                                break
1781
 
                if len(hits) >= 1:
1782
 
                    # if more than 1 hit with mesh, closest hit is new loc
1783
 
                    hits.sort()
1784
 
                    projection = hits[0][1]
1785
 
            if not projection:
1786
 
                # nothing to project on, remain at flat location
1787
 
                projection = loc[1]
1788
 
            new_locs.append([loc[0], projection])
1789
 
        
1790
 
        # return new positions of projected circle
1791
 
        return(new_locs)
1792
 
 
1793
 
 
1794
 
# check loops and only return valid ones
1795
 
def circle_check_loops(single_loops, loops, mapping, mesh_mod):
1796
 
    valid_single_loops = {}
1797
 
    valid_loops = []
1798
 
    for i, [loop, circular] in enumerate(loops):
1799
 
        # loop needs to have at least 3 vertices
1800
 
        if len(loop) < 3:
1801
 
            continue
1802
 
        # loop needs at least 1 vertex in the original, non-mirrored mesh
1803
 
        if mapping:
1804
 
            all_virtual = True
1805
 
            for vert in loop:
1806
 
                if mapping[vert] > -1:
1807
 
                    all_virtual = False
1808
 
                    break
1809
 
            if all_virtual:
1810
 
                continue
1811
 
        # loop has to be non-collinear
1812
 
        collinear = True
1813
 
        loc0 = mathutils.Vector(mesh_mod.vertices[loop[0]].co[:])
1814
 
        loc1 = mathutils.Vector(mesh_mod.vertices[loop[1]].co[:])
1815
 
        for v in loop[2:]:
1816
 
            locn = mathutils.Vector(mesh_mod.vertices[v].co[:])
1817
 
            if loc0 == loc1 or loc1 == locn:
1818
 
                loc0 = loc1
1819
 
                loc1 = locn
1820
 
                continue
1821
 
            d1 = loc1-loc0
1822
 
            d2 = locn-loc1
1823
 
            if -1e-6 < d1.angle(d2, 0) < 1e-6:
1824
 
                loc0 = loc1
1825
 
                loc1 = locn
1826
 
                continue
1827
 
            collinear = False
1828
 
            break
1829
 
        if collinear:
1830
 
            continue
1831
 
        # passed all tests, loop is valid
1832
 
        valid_loops.append([loop, circular])
1833
 
        valid_single_loops[len(valid_loops)-1] = single_loops[i]
1834
 
    
1835
 
    return(valid_single_loops, valid_loops)
1836
 
 
1837
 
 
1838
 
# calculate the location of single input vertices that need to be flattened
1839
 
def circle_flatten_singles(mesh_mod, com, p, q, normal, single_loop):
1840
 
    new_locs = []
1841
 
    for vert in single_loop:
1842
 
        loc = mathutils.Vector(mesh_mod.vertices[vert].co[:])
1843
 
        new_locs.append([vert,  loc - (loc-com).dot(normal)*normal])
1844
 
    
1845
 
    return(new_locs)
1846
 
 
1847
 
 
1848
 
# calculate input loops
1849
 
def circle_get_input(object, mesh, scene):
1850
 
    # get mesh with modifiers applied
1851
 
    derived, mesh_mod = get_derived_mesh(object, mesh, scene)
1852
 
    
1853
 
    # create list of edge-keys based on selection state
1854
 
    faces = False
1855
 
    for face in mesh.tessfaces:
1856
 
        if face.select and not face.hide:
1857
 
            faces = True
1858
 
            break
1859
 
    if faces:
1860
 
        # get selected, non-hidden , non-internal edge-keys
1861
 
        eks_selected = [key for keys in [face.edge_keys for face in \
1862
 
            mesh_mod.tessfaces if face.select and not face.hide] for key in keys]
1863
 
        edge_count = {}
1864
 
        for ek in eks_selected:
1865
 
            if ek in edge_count:
1866
 
                edge_count[ek] += 1
1867
 
            else:
1868
 
                edge_count[ek] = 1
1869
 
        edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \
1870
 
            and not edge.hide and edge_count.get(edge.key, 1)==1]
1871
 
    else:
1872
 
        # no faces, so no internal edges either
1873
 
        edge_keys = [edge.key for edge in mesh_mod.edges if edge.select \
1874
 
            and not edge.hide]
1875
 
    
1876
 
    # add edge-keys around single vertices
1877
 
    verts_connected = dict([[vert, 1] for edge in [edge for edge in \
1878
 
        mesh_mod.edges if edge.select and not edge.hide] for vert in edge.key])
1879
 
    single_vertices = [vert.index for vert in mesh_mod.vertices if \
1880
 
        vert.select and not vert.hide and not \
1881
 
        verts_connected.get(vert.index, False)]
1882
 
    
1883
 
    if single_vertices and len(mesh.tessfaces)>0:
1884
 
        vert_to_single = dict([[v.index, []] for v in mesh_mod.vertices \
1885
 
            if not v.hide])
1886
 
        for face in [face for face in mesh_mod.tessfaces if not face.select \
1887
 
        and not face.hide]:
1888
 
            for vert in face.vertices:
1889
 
                if vert in single_vertices:
1890
 
                    for ek in face.edge_keys:
1891
 
                        if not vert in ek:
1892
 
                            edge_keys.append(ek)
1893
 
                            if vert not in vert_to_single[ek[0]]:
1894
 
                                vert_to_single[ek[0]].append(vert)
1895
 
                            if vert not in vert_to_single[ek[1]]:
1896
 
                                vert_to_single[ek[1]].append(vert)
1897
 
                    break
1898
 
    
1899
 
    # sort edge-keys into loops
1900
 
    loops = get_connected_selections(edge_keys)
1901
 
    
1902
 
    # find out to which loops the single vertices belong
1903
 
    single_loops = dict([[i, []] for i in range(len(loops))])
1904
 
    if single_vertices and len(mesh.tessfaces)>0:
1905
 
        for i, [loop, circular] in enumerate(loops):
1906
 
            for vert in loop:
1907
 
                if vert_to_single[vert]:
1908
 
                    for single in vert_to_single[vert]:
1909
 
                        if single not in single_loops[i]:
1910
 
                            single_loops[i].append(single)
1911
 
    
1912
 
    return(derived, mesh_mod, single_vertices, single_loops, loops)
1913
 
 
1914
 
 
1915
 
# recalculate positions based on the influence of the circle shape
1916
 
def circle_influence_locs(locs_2d, new_locs_2d, influence):
1917
 
    for i in range(len(locs_2d)):
1918
 
        oldx, oldy, j = locs_2d[i]
1919
 
        newx, newy, k = new_locs_2d[i]
1920
 
        altx = newx*(influence/100)+ oldx*((100-influence)/100)
1921
 
        alty = newy*(influence/100)+ oldy*((100-influence)/100)
1922
 
        locs_2d[i] = [altx, alty, j]
1923
 
    
1924
 
    return(locs_2d)
1925
 
 
1926
 
 
1927
 
# project 2d locations on circle, respecting distance relations between verts
1928
 
def circle_project_non_regular(locs_2d, x0, y0, r):
1929
 
    for i in range(len(locs_2d)):
1930
 
        x, y, j = locs_2d[i]
1931
 
        loc = mathutils.Vector([x-x0, y-y0])
1932
 
        loc.length = r
1933
 
        locs_2d[i] = [loc[0], loc[1], j]
1934
 
    
1935
 
    return(locs_2d)
1936
 
 
1937
 
 
1938
 
# project 2d locations on circle, with equal distance between all vertices
1939
 
def circle_project_regular(locs_2d, x0, y0, r):
1940
 
    # find offset angle and circling direction
1941
 
    x, y, i = locs_2d[0]
1942
 
    loc = mathutils.Vector([x-x0, y-y0])
1943
 
    loc.length = r
1944
 
    offset_angle = loc.angle(mathutils.Vector([1.0, 0.0]), 0.0)
1945
 
    loca = mathutils.Vector([x-x0, y-y0, 0.0])
1946
 
    if loc[1] < -1e-6:
1947
 
        offset_angle *= -1
1948
 
    x, y, j = locs_2d[1]
1949
 
    locb = mathutils.Vector([x-x0, y-y0, 0.0])
1950
 
    if loca.cross(locb)[2] >= 0:
1951
 
        ccw = 1
1952
 
    else:
1953
 
        ccw = -1
1954
 
    # distribute vertices along the circle
1955
 
    for i in range(len(locs_2d)):
1956
 
        t = offset_angle + ccw * (i / len(locs_2d) * 2 * math.pi)
1957
 
        x = math.cos(t) * r
1958
 
        y = math.sin(t) * r
1959
 
        locs_2d[i] = [x, y, locs_2d[i][2]]
1960
 
    
1961
 
    return(locs_2d)
1962
 
 
1963
 
 
1964
 
# shift loop, so the first vertex is closest to the center
1965
 
def circle_shift_loop(mesh_mod, loop, com):
1966
 
    verts, circular = loop
1967
 
    distances = [[(mesh_mod.vertices[vert].co - com).length, i] \
1968
 
        for i, vert in enumerate(verts)]
1969
 
    distances.sort()
1970
 
    shift = distances[0][1]
1971
 
    loop = [verts[shift:] + verts[:shift], circular]
1972
 
    
1973
 
    return(loop)
1974
 
 
1975
 
 
1976
 
##########################################
1977
 
####### Curve functions ##################
1978
 
##########################################
1979
 
 
1980
 
# create lists with knots and points, all correctly sorted
1981
 
def curve_calculate_knots(loop, verts_selected):
1982
 
    knots = [v for v in loop[0] if v in verts_selected]
1983
 
    points = loop[0][:]
1984
 
    # circular loop, potential for weird splines
1985
 
    if loop[1]:
1986
 
        offset = int(len(loop[0]) / 4)
1987
 
        kpos = []
1988
 
        for k in knots:
1989
 
            kpos.append(loop[0].index(k))
1990
 
        kdif = []
1991
 
        for i in range(len(kpos) - 1):
1992
 
            kdif.append(kpos[i+1] - kpos[i])
1993
 
        kdif.append(len(loop[0]) - kpos[-1] + kpos[0])
1994
 
        kadd = []
1995
 
        for k in kdif:
1996
 
            if k > 2 * offset:
1997
 
                kadd.append([kdif.index(k), True])
1998
 
            # next 2 lines are optional, they insert
1999
 
            # an extra control point in small gaps
2000
 
            #elif k > offset:
2001
 
            #   kadd.append([kdif.index(k), False])
2002
 
        kins = []
2003
 
        krot = False
2004
 
        for k in kadd: # extra knots to be added
2005
 
            if k[1]: # big gap (break circular spline)
2006
 
                kpos = loop[0].index(knots[k[0]]) + offset
2007
 
                if kpos > len(loop[0]) - 1:
2008
 
                    kpos -= len(loop[0])
2009
 
                kins.append([knots[k[0]], loop[0][kpos]])
2010
 
                kpos2 = k[0] + 1
2011
 
                if kpos2 > len(knots)-1:
2012
 
                    kpos2 -= len(knots)
2013
 
                kpos2 = loop[0].index(knots[kpos2]) - offset
2014
 
                if kpos2 < 0:
2015
 
                    kpos2 += len(loop[0])
2016
 
                kins.append([loop[0][kpos], loop[0][kpos2]])
2017
 
                krot = loop[0][kpos2]
2018
 
            else: # small gap (keep circular spline)
2019
 
                k1 = loop[0].index(knots[k[0]])
2020
 
                k2 = k[0] + 1
2021
 
                if k2 > len(knots)-1:
2022
 
                    k2 -= len(knots)
2023
 
                k2 = loop[0].index(knots[k2])
2024
 
                if k2 < k1:
2025
 
                    dif = len(loop[0]) - 1 - k1 + k2
2026
 
                else:
2027
 
                    dif = k2 - k1
2028
 
                kn = k1 + int(dif/2)
2029
 
                if kn > len(loop[0]) - 1:
2030
 
                    kn -= len(loop[0])
2031
 
                kins.append([loop[0][k1], loop[0][kn]])
2032
 
        for j in kins: # insert new knots
2033
 
            knots.insert(knots.index(j[0]) + 1, j[1])
2034
 
        if not krot: # circular loop
2035
 
            knots.append(knots[0])
2036
 
            points = loop[0][loop[0].index(knots[0]):]
2037
 
            points += loop[0][0:loop[0].index(knots[0]) + 1]
2038
 
        else: # non-circular loop (broken by script)
2039
 
            krot = knots.index(krot)
2040
 
            knots = knots[krot:] + knots[0:krot]
2041
 
            if loop[0].index(knots[0]) > loop[0].index(knots[-1]):
2042
 
                points = loop[0][loop[0].index(knots[0]):]
2043
 
                points += loop[0][0:loop[0].index(knots[-1])+1]
2044
 
            else:
2045
 
                points = loop[0][loop[0].index(knots[0]):\
2046
 
                    loop[0].index(knots[-1]) + 1]
2047
 
    # non-circular loop, add first and last point as knots
2048
 
    else:
2049
 
        if loop[0][0] not in knots:
2050
 
            knots.insert(0, loop[0][0])
2051
 
        if loop[0][-1] not in knots:
2052
 
            knots.append(loop[0][-1])
2053
 
    
2054
 
    return(knots, points)
2055
 
 
2056
 
 
2057
 
# calculate relative positions compared to first knot
2058
 
def curve_calculate_t(mesh_mod, knots, points, pknots, regular, circular):
2059
 
    tpoints = []
2060
 
    loc_prev = False
2061
 
    len_total = 0
2062
 
    
2063
 
    for p in points:
2064
 
        if p in knots:
2065
 
            loc = pknots[knots.index(p)] # use projected knot location
2066
 
        else:
2067
 
            loc = mathutils.Vector(mesh_mod.vertices[p].co[:])
2068
 
        if not loc_prev:
2069
 
            loc_prev = loc
2070
 
        len_total += (loc-loc_prev).length
2071
 
        tpoints.append(len_total)
2072
 
        loc_prev = loc
2073
 
    tknots = []
2074
 
    for p in points:
2075
 
        if p in knots:
2076
 
            tknots.append(tpoints[points.index(p)])
2077
 
    if circular:
2078
 
        tknots[-1] = tpoints[-1]
2079
 
    
2080
 
    # regular option
2081
 
    if regular:
2082
 
        tpoints_average = tpoints[-1] / (len(tpoints) - 1)
2083
 
        for i in range(1, len(tpoints) - 1):
2084
 
            tpoints[i] = i * tpoints_average
2085
 
        for i in range(len(knots)):
2086
 
            tknots[i] = tpoints[points.index(knots[i])]
2087
 
        if circular:
2088
 
            tknots[-1] = tpoints[-1]
2089
 
    
2090
 
    
2091
 
    return(tknots, tpoints)
2092
 
 
2093
 
 
2094
 
# change the location of non-selected points to their place on the spline
2095
 
def curve_calculate_vertices(mesh_mod, knots, tknots, points, tpoints, splines,
2096
 
interpolation, restriction):
2097
 
    newlocs = {}
2098
 
    move = []
2099
 
    
2100
 
    for p in points:
2101
 
        if p in knots:
2102
 
            continue
2103
 
        m = tpoints[points.index(p)]
2104
 
        if m in tknots:
2105
 
            n = tknots.index(m)
2106
 
        else:
2107
 
            t = tknots[:]
2108
 
            t.append(m)
2109
 
            t.sort()
2110
 
            n = t.index(m) - 1
2111
 
        if n > len(splines) - 1:
2112
 
            n = len(splines) - 1
2113
 
        elif n < 0:
2114
 
            n = 0
2115
 
        
2116
 
        if interpolation == 'cubic':
2117
 
            ax, bx, cx, dx, tx = splines[n][0]
2118
 
            x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2119
 
            ay, by, cy, dy, ty = splines[n][1]
2120
 
            y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2121
 
            az, bz, cz, dz, tz = splines[n][2]
2122
 
            z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2123
 
            newloc = mathutils.Vector([x,y,z])
2124
 
        else: # interpolation == 'linear'
2125
 
            a, d, t, u = splines[n]
2126
 
            newloc = ((m-t)/u)*d + a
2127
 
 
2128
 
        if restriction != 'none': # vertex movement is restricted
2129
 
            newlocs[p] = newloc
2130
 
        else: # set the vertex to its new location
2131
 
            move.append([p, newloc])
2132
 
        
2133
 
    if restriction != 'none': # vertex movement is restricted
2134
 
        for p in points:
2135
 
            if p in newlocs:
2136
 
                newloc = newlocs[p]
2137
 
            else:
2138
 
                move.append([p, mesh_mod.vertices[p].co])
2139
 
                continue
2140
 
            oldloc = mesh_mod.vertices[p].co
2141
 
            normal = mesh_mod.vertices[p].normal
2142
 
            dloc = newloc - oldloc
2143
 
            if dloc.length < 1e-6:
2144
 
                move.append([p, newloc])
2145
 
            elif restriction == 'extrude': # only extrusions
2146
 
                if dloc.angle(normal, 0) < 0.5 * math.pi + 1e-6:
2147
 
                    move.append([p, newloc])
2148
 
            else: # restriction == 'indent' only indentations
2149
 
                if dloc.angle(normal) > 0.5 * math.pi - 1e-6:
2150
 
                    move.append([p, newloc])
2151
 
 
2152
 
    return(move)
2153
 
 
2154
 
 
2155
 
# trim loops to part between first and last selected vertices (including)
2156
 
def curve_cut_boundaries(mesh_mod, loops):
2157
 
    cut_loops = []
2158
 
    for loop, circular in loops:
2159
 
        if circular:
2160
 
            # don't cut
2161
 
            cut_loops.append([loop, circular])
2162
 
            continue
2163
 
        selected = [mesh_mod.vertices[v].select for v in loop]
2164
 
        first = selected.index(True)
2165
 
        selected.reverse()
2166
 
        last = -selected.index(True)
2167
 
        if last == 0:
2168
 
            cut_loops.append([loop[first:], circular])
2169
 
        else:
2170
 
            cut_loops.append([loop[first:last], circular])
2171
 
    
2172
 
    return(cut_loops)
2173
 
 
2174
 
 
2175
 
# calculate input loops
2176
 
def curve_get_input(object, mesh, boundaries, scene):
2177
 
    # get mesh with modifiers applied
2178
 
    derived, mesh_mod = get_derived_mesh(object, mesh, scene)
2179
 
    
2180
 
    # vertices that still need a loop to run through it
2181
 
    verts_unsorted = [v.index for v in mesh_mod.vertices if \
2182
 
        v.select and not v.hide]
2183
 
    # necessary dictionaries
2184
 
    vert_edges = dict_vert_edges(mesh_mod)
2185
 
    edge_faces = dict_edge_faces(mesh_mod)
2186
 
    correct_loops = []
2187
 
    
2188
 
    # find loops through each selected vertex
2189
 
    while len(verts_unsorted) > 0:
2190
 
        loops = curve_vertex_loops(mesh_mod, verts_unsorted[0], vert_edges,
2191
 
            edge_faces)
2192
 
        verts_unsorted.pop(0)
2193
 
        
2194
 
        # check if loop is fully selected
2195
 
        search_perpendicular = False
2196
 
        i = -1
2197
 
        for loop, circular in loops:
2198
 
            i += 1
2199
 
            selected = [v for v in loop if mesh_mod.vertices[v].select]
2200
 
            if len(selected) < 2:
2201
 
                # only one selected vertex on loop, don't use
2202
 
                loops.pop(i)
2203
 
                continue
2204
 
            elif len(selected) == len(loop):
2205
 
                search_perpendicular = loop
2206
 
                break
2207
 
        # entire loop is selected, find perpendicular loops
2208
 
        if search_perpendicular:
2209
 
            for vert in loop:
2210
 
                if vert in verts_unsorted:
2211
 
                    verts_unsorted.remove(vert)
2212
 
            perp_loops = curve_perpendicular_loops(mesh_mod, loop,
2213
 
                vert_edges, edge_faces)
2214
 
            for perp_loop in perp_loops:
2215
 
                correct_loops.append(perp_loop)
2216
 
        # normal input
2217
 
        else:
2218
 
            for loop, circular in loops:
2219
 
                correct_loops.append([loop, circular])
2220
 
    
2221
 
    # boundaries option
2222
 
    if boundaries:
2223
 
        correct_loops = curve_cut_boundaries(mesh_mod, correct_loops)
2224
 
    
2225
 
    return(derived, mesh_mod, correct_loops)
2226
 
 
2227
 
 
2228
 
# return all loops that are perpendicular to the given one
2229
 
def curve_perpendicular_loops(mesh_mod, start_loop, vert_edges, edge_faces):
2230
 
    # find perpendicular loops
2231
 
    perp_loops = []
2232
 
    for start_vert in start_loop:
2233
 
        loops = curve_vertex_loops(mesh_mod, start_vert, vert_edges,
2234
 
            edge_faces)
2235
 
        for loop, circular in loops:
2236
 
            selected = [v for v in loop if mesh_mod.vertices[v].select]
2237
 
            if len(selected) == len(loop):
2238
 
                continue
2239
 
            else:
2240
 
                perp_loops.append([loop, circular, loop.index(start_vert)])
2241
 
    
2242
 
    # trim loops to same lengths
2243
 
    shortest = [[len(loop[0]), i] for i, loop in enumerate(perp_loops)\
2244
 
        if not loop[1]]
2245
 
    if not shortest:
2246
 
        # all loops are circular, not trimming
2247
 
        return([[loop[0], loop[1]] for loop in perp_loops])
2248
 
    else:
2249
 
        shortest = min(shortest)
2250
 
    shortest_start = perp_loops[shortest[1]][2]
2251
 
    before_start = shortest_start
2252
 
    after_start = shortest[0] - shortest_start - 1
2253
 
    bigger_before = before_start > after_start
2254
 
    trimmed_loops = []
2255
 
    for loop in perp_loops:
2256
 
        # have the loop face the same direction as the shortest one
2257
 
        if bigger_before:
2258
 
            if loop[2] < len(loop[0]) / 2:
2259
 
                loop[0].reverse()
2260
 
                loop[2] = len(loop[0]) - loop[2] - 1
2261
 
        else:
2262
 
            if loop[2] > len(loop[0]) / 2:
2263
 
                loop[0].reverse()
2264
 
                loop[2] = len(loop[0]) - loop[2] - 1
2265
 
        # circular loops can shift, to prevent wrong trimming
2266
 
        if loop[1]:
2267
 
            shift = shortest_start - loop[2]
2268
 
            if loop[2] + shift > 0 and loop[2] + shift < len(loop[0]):
2269
 
                loop[0] = loop[0][-shift:] + loop[0][:-shift]
2270
 
            loop[2] += shift
2271
 
            if loop[2] < 0:
2272
 
                loop[2] += len(loop[0])
2273
 
            elif loop[2] > len(loop[0]) -1:
2274
 
                loop[2] -= len(loop[0])
2275
 
        # trim
2276
 
        start = max(0, loop[2] - before_start)
2277
 
        end = min(len(loop[0]), loop[2] + after_start + 1)
2278
 
        trimmed_loops.append([loop[0][start:end], False])
2279
 
    
2280
 
    return(trimmed_loops)
2281
 
 
2282
 
 
2283
 
# project knots on non-selected geometry
2284
 
def curve_project_knots(mesh_mod, verts_selected, knots, points, circular):
2285
 
    # function to project vertex on edge
2286
 
    def project(v1, v2, v3):
2287
 
        # v1 and v2 are part of a line
2288
 
        # v3 is projected onto it
2289
 
        v2 -= v1
2290
 
        v3 -= v1
2291
 
        p = v3.project(v2)
2292
 
        return(p + v1)
2293
 
    
2294
 
    if circular: # project all knots
2295
 
        start = 0
2296
 
        end = len(knots)
2297
 
        pknots = []
2298
 
    else: # first and last knot shouldn't be projected
2299
 
        start = 1
2300
 
        end = -1
2301
 
        pknots = [mathutils.Vector(mesh_mod.vertices[knots[0]].co[:])]
2302
 
    for knot in knots[start:end]:
2303
 
        if knot in verts_selected:
2304
 
            knot_left = knot_right = False
2305
 
            for i in range(points.index(knot)-1, -1*len(points), -1):
2306
 
                if points[i] not in knots:
2307
 
                    knot_left = points[i]
2308
 
                    break
2309
 
            for i in range(points.index(knot)+1, 2*len(points)):
2310
 
                if i > len(points) - 1:
2311
 
                    i -= len(points)
2312
 
                if points[i] not in knots:
2313
 
                    knot_right = points[i]
2314
 
                    break
2315
 
            if knot_left and knot_right and knot_left != knot_right:
2316
 
                knot_left = mathutils.Vector(\
2317
 
                    mesh_mod.vertices[knot_left].co[:])
2318
 
                knot_right = mathutils.Vector(\
2319
 
                    mesh_mod.vertices[knot_right].co[:])
2320
 
                knot = mathutils.Vector(mesh_mod.vertices[knot].co[:])
2321
 
                pknots.append(project(knot_left, knot_right, knot))
2322
 
            else:
2323
 
                pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:]))
2324
 
        else: # knot isn't selected, so shouldn't be changed
2325
 
            pknots.append(mathutils.Vector(mesh_mod.vertices[knot].co[:]))
2326
 
    if not circular:
2327
 
        pknots.append(mathutils.Vector(mesh_mod.vertices[knots[-1]].co[:]))
2328
 
    
2329
 
    return(pknots)
2330
 
 
2331
 
 
2332
 
# find all loops through a given vertex
2333
 
def curve_vertex_loops(mesh_mod, start_vert, vert_edges, edge_faces):
2334
 
    edges_used = []
2335
 
    loops = []
2336
 
        
2337
 
    for edge in vert_edges[start_vert]:
2338
 
        if edge in edges_used:
2339
 
            continue
2340
 
        loop = []
2341
 
        circular = False
2342
 
        for vert in edge:
2343
 
            active_faces = edge_faces[edge]
2344
 
            new_vert = vert
2345
 
            growing = True
2346
 
            while growing:
2347
 
                growing = False
2348
 
                new_edges = vert_edges[new_vert]
2349
 
                loop.append(new_vert)
2350
 
                if len(loop) > 1:
2351
 
                    edges_used.append(tuple(sorted([loop[-1], loop[-2]])))
2352
 
                if len(new_edges) < 3 or len(new_edges) > 4:
2353
 
                    # pole
2354
 
                    break
2355
 
                else:
2356
 
                    # find next edge
2357
 
                    for new_edge in new_edges:
2358
 
                        if new_edge in edges_used:
2359
 
                            continue
2360
 
                        eliminate = False
2361
 
                        for new_face in edge_faces[new_edge]:
2362
 
                            if new_face in active_faces:
2363
 
                                eliminate = True
2364
 
                                break
2365
 
                        if eliminate:
2366
 
                            continue
2367
 
                        # found correct new edge
2368
 
                        active_faces = edge_faces[new_edge]
2369
 
                        v1, v2 = new_edge
2370
 
                        if v1 != new_vert:
2371
 
                            new_vert = v1
2372
 
                        else:
2373
 
                            new_vert = v2
2374
 
                        if new_vert == loop[0]:
2375
 
                            circular = True
2376
 
                        else:
2377
 
                            growing = True
2378
 
                        break
2379
 
            if circular:
2380
 
                break
2381
 
            loop.reverse()
2382
 
        loops.append([loop, circular])
2383
 
    
2384
 
    return(loops)
2385
 
 
2386
 
 
2387
 
##########################################
2388
 
####### Flatten functions ################
2389
 
##########################################
2390
 
 
2391
 
# sort input into loops
2392
 
def flatten_get_input(mesh):
2393
 
    vert_verts = dict_vert_verts([edge.key for edge in mesh.edges \
2394
 
        if edge.select and not edge.hide])
2395
 
    verts = [v.index for v in mesh.vertices if v.select and not v.hide]
2396
 
    
2397
 
    # no connected verts, consider all selected verts as a single input
2398
 
    if not vert_verts:
2399
 
        return([[verts, False]])
2400
 
    
2401
 
    loops = []
2402
 
    while len(verts) > 0:
2403
 
        # start of loop
2404
 
        loop = [verts[0]]
2405
 
        verts.pop(0)
2406
 
        if loop[-1] in vert_verts:
2407
 
            to_grow = vert_verts[loop[-1]]
2408
 
        else:
2409
 
            to_grow = []
2410
 
        # grow loop
2411
 
        while len(to_grow) > 0:
2412
 
            new_vert = to_grow[0]
2413
 
            to_grow.pop(0)
2414
 
            if new_vert in loop:
2415
 
                continue
2416
 
            loop.append(new_vert)
2417
 
            verts.remove(new_vert)
2418
 
            to_grow += vert_verts[new_vert]
2419
 
        # add loop to loops
2420
 
        loops.append([loop, False])
2421
 
    
2422
 
    return(loops)
2423
 
 
2424
 
 
2425
 
# calculate position of vertex projections on plane
2426
 
def flatten_project(mesh, loop, com, normal):
2427
 
    verts = [mesh.vertices[v] for v in loop[0]]
2428
 
    verts_projected = [[v.index, mathutils.Vector(v.co[:]) - \
2429
 
        (mathutils.Vector(v.co[:])-com).dot(normal)*normal] for v in verts]
2430
 
    
2431
 
    return(verts_projected)
2432
 
 
2433
 
 
2434
 
##########################################
2435
 
####### Relax functions ##################
2436
 
##########################################
2437
 
 
2438
 
# create lists with knots and points, all correctly sorted
2439
 
def relax_calculate_knots(loops):
2440
 
    all_knots = []
2441
 
    all_points = []
2442
 
    for loop, circular in loops:
2443
 
        knots = [[], []]
2444
 
        points = [[], []]
2445
 
        if circular:
2446
 
            if len(loop)%2 == 1: # odd
2447
 
                extend = [False, True, 0, 1, 0, 1]
2448
 
            else: # even
2449
 
                extend = [True, False, 0, 1, 1, 2]
2450
 
        else:
2451
 
            if len(loop)%2 == 1: # odd
2452
 
                extend = [False, False, 0, 1, 1, 2]
2453
 
            else: # even
2454
 
                extend = [False, False, 0, 1, 1, 2]
2455
 
        for j in range(2):
2456
 
            if extend[j]:
2457
 
                loop = [loop[-1]] + loop + [loop[0]]
2458
 
            for i in range(extend[2+2*j], len(loop), 2):
2459
 
                knots[j].append(loop[i])
2460
 
            for i in range(extend[3+2*j], len(loop), 2):
2461
 
                if loop[i] == loop[-1] and not circular:
2462
 
                    continue
2463
 
                if len(points[j]) == 0:
2464
 
                    points[j].append(loop[i])
2465
 
                elif loop[i] != points[j][0]:
2466
 
                    points[j].append(loop[i])
2467
 
            if circular:
2468
 
                if knots[j][0] != knots[j][-1]:
2469
 
                    knots[j].append(knots[j][0])
2470
 
        if len(points[1]) == 0:
2471
 
            knots.pop(1)
2472
 
            points.pop(1)
2473
 
        for k in knots:
2474
 
            all_knots.append(k)
2475
 
        for p in points:
2476
 
            all_points.append(p)
2477
 
    
2478
 
    return(all_knots, all_points)
2479
 
 
2480
 
 
2481
 
# calculate relative positions compared to first knot
2482
 
def relax_calculate_t(mesh_mod, knots, points, regular):
2483
 
    all_tknots = []
2484
 
    all_tpoints = []
2485
 
    for i in range(len(knots)):
2486
 
        amount = len(knots[i]) + len(points[i])
2487
 
        mix  = []
2488
 
        for j in range(amount):
2489
 
            if j%2 == 0:
2490
 
                mix.append([True, knots[i][round(j/2)]])
2491
 
            elif j == amount-1:
2492
 
                mix.append([True, knots[i][-1]])
2493
 
            else:
2494
 
                mix.append([False, points[i][int(j/2)]])
2495
 
        len_total = 0
2496
 
        loc_prev = False
2497
 
        tknots = []
2498
 
        tpoints = []
2499
 
        for m in mix:
2500
 
            loc = mathutils.Vector(mesh_mod.vertices[m[1]].co[:])
2501
 
            if not loc_prev:
2502
 
                loc_prev = loc
2503
 
            len_total += (loc - loc_prev).length
2504
 
            if m[0]:
2505
 
                tknots.append(len_total)
2506
 
            else:
2507
 
                tpoints.append(len_total)
2508
 
            loc_prev = loc
2509
 
        if regular:
2510
 
            tpoints = []
2511
 
            for p in range(len(points[i])):
2512
 
                tpoints.append((tknots[p] + tknots[p+1]) / 2)
2513
 
        all_tknots.append(tknots)
2514
 
        all_tpoints.append(tpoints)
2515
 
    
2516
 
    return(all_tknots, all_tpoints)
2517
 
 
2518
 
 
2519
 
# change the location of the points to their place on the spline
2520
 
def relax_calculate_verts(mesh_mod, interpolation, tknots, knots, tpoints,
2521
 
points, splines):
2522
 
    change = []
2523
 
    move = []
2524
 
    for i in range(len(knots)):
2525
 
        for p in points[i]:
2526
 
            m = tpoints[i][points[i].index(p)]
2527
 
            if m in tknots[i]:
2528
 
                n = tknots[i].index(m)
2529
 
            else:
2530
 
                t = tknots[i][:]
2531
 
                t.append(m)
2532
 
                t.sort()
2533
 
                n = t.index(m)-1
2534
 
            if n > len(splines[i]) - 1:
2535
 
                n = len(splines[i]) - 1
2536
 
            elif n < 0:
2537
 
                n = 0
2538
 
            
2539
 
            if interpolation == 'cubic':
2540
 
                ax, bx, cx, dx, tx = splines[i][n][0]
2541
 
                x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2542
 
                ay, by, cy, dy, ty = splines[i][n][1]
2543
 
                y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2544
 
                az, bz, cz, dz, tz = splines[i][n][2]
2545
 
                z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2546
 
                change.append([p, mathutils.Vector([x,y,z])])
2547
 
            else: # interpolation == 'linear'
2548
 
                a, d, t, u = splines[i][n]
2549
 
                if u == 0:
2550
 
                    u = 1e-8
2551
 
                change.append([p, ((m-t)/u)*d + a])
2552
 
    for c in change:
2553
 
        move.append([c[0], (mesh_mod.vertices[c[0]].co + c[1]) / 2])
2554
 
    
2555
 
    return(move)
2556
 
 
2557
 
 
2558
 
##########################################
2559
 
####### Space functions ##################
2560
 
##########################################
2561
 
 
2562
 
# calculate relative positions compared to first knot
2563
 
def space_calculate_t(mesh_mod, knots):
2564
 
    tknots = []
2565
 
    loc_prev = False
2566
 
    len_total = 0
2567
 
    for k in knots:
2568
 
        loc = mathutils.Vector(mesh_mod.vertices[k].co[:])
2569
 
        if not loc_prev:
2570
 
            loc_prev = loc
2571
 
        len_total += (loc - loc_prev).length
2572
 
        tknots.append(len_total)
2573
 
        loc_prev = loc
2574
 
    amount = len(knots)
2575
 
    t_per_segment = len_total / (amount - 1)
2576
 
    tpoints = [i * t_per_segment for i in range(amount)]
2577
 
    
2578
 
    return(tknots, tpoints)
2579
 
 
2580
 
 
2581
 
# change the location of the points to their place on the spline
2582
 
def space_calculate_verts(mesh_mod, interpolation, tknots, tpoints, points,
2583
 
splines):
2584
 
    move = []
2585
 
    for p in points:
2586
 
        m = tpoints[points.index(p)]
2587
 
        if m in tknots:
2588
 
            n = tknots.index(m)
2589
 
        else:
2590
 
            t = tknots[:]
2591
 
            t.append(m)
2592
 
            t.sort()
2593
 
            n = t.index(m) - 1
2594
 
        if n > len(splines) - 1:
2595
 
            n = len(splines) - 1
2596
 
        elif n < 0:
2597
 
            n = 0
2598
 
        
2599
 
        if interpolation == 'cubic':
2600
 
            ax, bx, cx, dx, tx = splines[n][0]
2601
 
            x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
2602
 
            ay, by, cy, dy, ty = splines[n][1]
2603
 
            y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
2604
 
            az, bz, cz, dz, tz = splines[n][2]
2605
 
            z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
2606
 
            move.append([p, mathutils.Vector([x,y,z])])
2607
 
        else: # interpolation == 'linear'
2608
 
            a, d, t, u = splines[n]
2609
 
            move.append([p, ((m-t)/u)*d + a])
2610
 
    
2611
 
    return(move)
2612
 
 
2613
 
 
2614
 
##########################################
2615
 
####### Operators ########################
2616
 
##########################################
2617
 
 
2618
 
# bridge operator
2619
 
class Bridge(bpy.types.Operator):
2620
 
    bl_idname = 'mesh.looptools_bridge'
2621
 
    bl_label = "Bridge / Loft"
2622
 
    bl_description = "Bridge two, or loft several, loops of vertices"
2623
 
    bl_options = {'REGISTER', 'UNDO'}
2624
 
    
2625
 
    cubic_strength = bpy.props.FloatProperty(name = "Strength",
2626
 
        description = "Higher strength results in more fluid curves",
2627
 
        default = 1.0,
2628
 
        soft_min = -3.0,
2629
 
        soft_max = 3.0)
2630
 
    interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
2631
 
        items = (('cubic', "Cubic", "Gives curved results"),
2632
 
            ('linear', "Linear", "Basic, fast, straight interpolation")),
2633
 
        description = "Interpolation mode: algorithm used when creating "\
2634
 
            "segments",
2635
 
        default = 'cubic')
2636
 
    loft = bpy.props.BoolProperty(name = "Loft",
2637
 
        description = "Loft multiple loops, instead of considering them as "\
2638
 
            "a multi-input for bridging",
2639
 
        default = False)
2640
 
    loft_loop = bpy.props.BoolProperty(name = "Loop",
2641
 
        description = "Connect the first and the last loop with each other",
2642
 
        default = False)
2643
 
    min_width = bpy.props.IntProperty(name = "Minimum width",
2644
 
        description = "Segments with an edge smaller than this are merged "\
2645
 
            "(compared to base edge)",
2646
 
        default = 0,
2647
 
        min = 0,
2648
 
        max = 100,
2649
 
        subtype = 'PERCENTAGE')
2650
 
    mode = bpy.props.EnumProperty(name = "Mode",
2651
 
        items = (('basic', "Basic", "Fast algorithm"), ('shortest',
2652
 
            "Shortest edge", "Slower algorithm with better vertex matching")),
2653
 
        description = "Algorithm used for bridging",
2654
 
        default = 'shortest')
2655
 
    remove_faces = bpy.props.BoolProperty(name = "Remove faces",
2656
 
        description = "Remove faces that are internal after bridging",
2657
 
        default = True)
2658
 
    reverse = bpy.props.BoolProperty(name = "Reverse",
2659
 
        description = "Manually override the direction in which the loops "\
2660
 
                      "are bridged. Only use if the tool gives the wrong " \
2661
 
                      "result",
2662
 
        default = False)
2663
 
    segments = bpy.props.IntProperty(name = "Segments",
2664
 
        description = "Number of segments used to bridge the gap "\
2665
 
            "(0 = automatic)",
2666
 
        default = 1,
2667
 
        min = 0,
2668
 
        soft_max = 20)
2669
 
    twist = bpy.props.IntProperty(name = "Twist",
2670
 
        description = "Twist what vertices are connected to each other",
2671
 
        default = 0)
2672
 
    
2673
 
    @classmethod
2674
 
    def poll(cls, context):
2675
 
        ob = context.active_object
2676
 
        return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2677
 
    
2678
 
    def draw(self, context):
2679
 
        layout = self.layout
2680
 
        #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
2681
 
        
2682
 
        # top row
2683
 
        col_top = layout.column(align=True)
2684
 
        row = col_top.row(align=True)
2685
 
        col_left = row.column(align=True)
2686
 
        col_right = row.column(align=True)
2687
 
        col_right.active = self.segments != 1
2688
 
        col_left.prop(self, "segments")
2689
 
        col_right.prop(self, "min_width", text="")
2690
 
        # bottom row
2691
 
        bottom_left = col_left.row()
2692
 
        bottom_left.active = self.segments != 1
2693
 
        bottom_left.prop(self, "interpolation", text="")
2694
 
        bottom_right = col_right.row()
2695
 
        bottom_right.active = self.interpolation == 'cubic'
2696
 
        bottom_right.prop(self, "cubic_strength")
2697
 
        # boolean properties
2698
 
        col_top.prop(self, "remove_faces")
2699
 
        if self.loft:
2700
 
            col_top.prop(self, "loft_loop")
2701
 
        
2702
 
        # override properties
2703
 
        col_top.separator()
2704
 
        row = layout.row(align = True)
2705
 
        row.prop(self, "twist")
2706
 
        row.prop(self, "reverse")
2707
 
    
2708
 
    def invoke(self, context, event):
2709
 
        # load custom settings
2710
 
        context.window_manager.looptools.bridge_loft = self.loft
2711
 
        settings_load(self)
2712
 
        return self.execute(context)
2713
 
    
2714
 
    def execute(self, context):
2715
 
        # initialise
2716
 
        global_undo, object, mesh = initialise()
2717
 
        edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
2718
 
            bridge_initialise(mesh, self.interpolation)
2719
 
        settings_write(self)
2720
 
        
2721
 
        # check cache to see if we can save time
2722
 
        input_method = bridge_input_method(self.loft, self.loft_loop)
2723
 
        cached, single_loops, loops, derived, mapping = cache_read("Bridge",
2724
 
            object, mesh, input_method, False)
2725
 
        if not cached:
2726
 
            # get loops
2727
 
            loops = bridge_get_input(mesh)
2728
 
            if loops:
2729
 
                # reorder loops if there are more than 2
2730
 
                if len(loops) > 2:
2731
 
                    if self.loft:
2732
 
                        loops = bridge_sort_loops(mesh, loops, self.loft_loop)
2733
 
                    else:
2734
 
                        loops = bridge_match_loops(mesh, loops)
2735
 
        
2736
 
        # saving cache for faster execution next time
2737
 
        if not cached:
2738
 
            cache_write("Bridge", object, mesh, input_method, False, False,
2739
 
                loops, False, False)
2740
 
        
2741
 
        if loops:
2742
 
            # calculate new geometry
2743
 
            vertices = []
2744
 
            faces = []
2745
 
            max_vert_index = len(mesh.vertices)-1
2746
 
            for i in range(1, len(loops)):
2747
 
                if not self.loft and i%2 == 0:
2748
 
                    continue
2749
 
                lines = bridge_calculate_lines(mesh, loops[i-1:i+1],
2750
 
                    self.mode, self.twist, self.reverse)
2751
 
                vertex_normals = bridge_calculate_virtual_vertex_normals(mesh,
2752
 
                    lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
2753
 
                segments = bridge_calculate_segments(mesh, lines,
2754
 
                    loops[i-1:i+1], self.segments)
2755
 
                new_verts, new_faces, max_vert_index = \
2756
 
                    bridge_calculate_geometry(mesh, lines, vertex_normals,
2757
 
                    segments, self.interpolation, self.cubic_strength,
2758
 
                    self.min_width, max_vert_index)
2759
 
                if new_verts:
2760
 
                    vertices += new_verts
2761
 
                if new_faces:
2762
 
                    faces += new_faces
2763
 
            # make sure faces in loops that aren't used, aren't removed
2764
 
            if self.remove_faces and old_selected_faces:
2765
 
                bridge_save_unused_faces(mesh, old_selected_faces, loops)
2766
 
            # create vertices
2767
 
            if vertices:
2768
 
                bridge_create_vertices(mesh, vertices)
2769
 
            # create faces
2770
 
            if faces:
2771
 
                bridge_create_faces(mesh, faces, self.twist)
2772
 
                bridge_select_new_faces(mesh, len(faces), smooth)
2773
 
            # edge-data could have changed, can't use cache next run
2774
 
            if faces and not vertices:
2775
 
                cache_delete("Bridge")
2776
 
            # delete internal faces
2777
 
            if self.remove_faces and old_selected_faces:
2778
 
                bridge_remove_internal_faces(mesh, old_selected_faces)
2779
 
            # make sure normals are facing outside
2780
 
            bridge_recalculate_normals()
2781
 
        
2782
 
        terminate(global_undo)
2783
 
        return{'FINISHED'}
2784
 
 
2785
 
 
2786
 
# circle operator
2787
 
class Circle(bpy.types.Operator):
2788
 
    bl_idname = "mesh.looptools_circle"
2789
 
    bl_label = "Circle"
2790
 
    bl_description = "Move selected vertices into a circle shape"
2791
 
    bl_options = {'REGISTER', 'UNDO'}
2792
 
    
2793
 
    custom_radius = bpy.props.BoolProperty(name = "Radius",
2794
 
        description = "Force a custom radius",
2795
 
        default = False)
2796
 
    fit = bpy.props.EnumProperty(name = "Method",
2797
 
        items = (("best", "Best fit", "Non-linear least squares"),
2798
 
            ("inside", "Fit inside","Only move vertices towards the center")),
2799
 
        description = "Method used for fitting a circle to the vertices",
2800
 
        default = 'best')
2801
 
    flatten = bpy.props.BoolProperty(name = "Flatten",
2802
 
        description = "Flatten the circle, instead of projecting it on the " \
2803
 
            "mesh",
2804
 
        default = True)
2805
 
    influence = bpy.props.FloatProperty(name = "Influence",
2806
 
        description = "Force of the tool",
2807
 
        default = 100.0,
2808
 
        min = 0.0,
2809
 
        max = 100.0,
2810
 
        precision = 1,
2811
 
        subtype = 'PERCENTAGE')
2812
 
    radius = bpy.props.FloatProperty(name = "Radius",
2813
 
        description = "Custom radius for circle",
2814
 
        default = 1.0,
2815
 
        min = 0.0,
2816
 
        soft_max = 1000.0)
2817
 
    regular = bpy.props.BoolProperty(name = "Regular",
2818
 
        description = "Distribute vertices at constant distances along the " \
2819
 
            "circle",
2820
 
        default = True)
2821
 
    
2822
 
    @classmethod
2823
 
    def poll(cls, context):
2824
 
        ob = context.active_object
2825
 
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2826
 
    
2827
 
    def draw(self, context):
2828
 
        layout = self.layout
2829
 
        col = layout.column()
2830
 
        
2831
 
        col.prop(self, "fit")
2832
 
        col.separator()
2833
 
        
2834
 
        col.prop(self, "flatten")
2835
 
        row = col.row(align=True)
2836
 
        row.prop(self, "custom_radius")
2837
 
        row_right = row.row(align=True)
2838
 
        row_right.active = self.custom_radius
2839
 
        row_right.prop(self, "radius", text="")
2840
 
        col.prop(self, "regular")
2841
 
        col.separator()
2842
 
                
2843
 
        col.prop(self, "influence")
2844
 
    
2845
 
    def invoke(self, context, event):
2846
 
        # load custom settings
2847
 
        settings_load(self)
2848
 
        return self.execute(context)
2849
 
    
2850
 
    def execute(self, context):
2851
 
        # initialise
2852
 
        global_undo, object, mesh = initialise()
2853
 
        settings_write(self)
2854
 
        # check cache to see if we can save time
2855
 
        cached, single_loops, loops, derived, mapping = cache_read("Circle",
2856
 
            object, mesh, False, False)
2857
 
        if cached:
2858
 
            derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
2859
 
        else:
2860
 
            # find loops
2861
 
            derived, mesh_mod, single_vertices, single_loops, loops = \
2862
 
                circle_get_input(object, mesh, context.scene)
2863
 
            mapping = get_mapping(derived, mesh, mesh_mod, single_vertices,
2864
 
                False, loops)
2865
 
            single_loops, loops = circle_check_loops(single_loops, loops,
2866
 
                mapping, mesh_mod)
2867
 
        
2868
 
        # saving cache for faster execution next time
2869
 
        if not cached:
2870
 
            cache_write("Circle", object, mesh, False, False, single_loops,
2871
 
                loops, derived, mapping)
2872
 
        
2873
 
        move = []
2874
 
        for i, loop in enumerate(loops):
2875
 
            # best fitting flat plane
2876
 
            com, normal = calculate_plane(mesh_mod, loop)
2877
 
            # if circular, shift loop so we get a good starting vertex
2878
 
            if loop[1]:
2879
 
                loop = circle_shift_loop(mesh_mod, loop, com)
2880
 
            # flatten vertices on plane
2881
 
            locs_2d, p, q = circle_3d_to_2d(mesh_mod, loop, com, normal)
2882
 
            # calculate circle
2883
 
            if self.fit == 'best':
2884
 
                x0, y0, r = circle_calculate_best_fit(locs_2d)
2885
 
            else: # self.fit == 'inside'
2886
 
                x0, y0, r = circle_calculate_min_fit(locs_2d)
2887
 
            # radius override
2888
 
            if self.custom_radius:
2889
 
                r = self.radius / p.length
2890
 
            # calculate positions on circle
2891
 
            if self.regular:
2892
 
                new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
2893
 
            else:
2894
 
                new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
2895
 
            # take influence into account
2896
 
            locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
2897
 
                self.influence)
2898
 
            # calculate 3d positions of the created 2d input
2899
 
            move.append(circle_calculate_verts(self.flatten, mesh_mod,
2900
 
                locs_2d, com, p, q, normal))
2901
 
            # flatten single input vertices on plane defined by loop
2902
 
            if self.flatten and single_loops:
2903
 
                move.append(circle_flatten_singles(mesh_mod, com, p, q,
2904
 
                    normal, single_loops[i]))
2905
 
        
2906
 
        # move vertices to new locations
2907
 
        move_verts(mesh, mapping, move, -1)
2908
 
        
2909
 
        # cleaning up 
2910
 
        if derived:
2911
 
            bpy.context.blend_data.meshes.remove(mesh_mod)
2912
 
        terminate(global_undo)
2913
 
        
2914
 
        return{'FINISHED'}
2915
 
 
2916
 
 
2917
 
# curve operator
2918
 
class Curve(bpy.types.Operator):
2919
 
    bl_idname = "mesh.looptools_curve"
2920
 
    bl_label = "Curve"
2921
 
    bl_description = "Turn a loop into a smooth curve"
2922
 
    bl_options = {'REGISTER', 'UNDO'}
2923
 
    
2924
 
    boundaries = bpy.props.BoolProperty(name = "Boundaries",
2925
 
        description = "Limit the tool to work within the boundaries of the "\
2926
 
            "selected vertices",
2927
 
        default = False)
2928
 
    influence = bpy.props.FloatProperty(name = "Influence",
2929
 
        description = "Force of the tool",
2930
 
        default = 100.0,
2931
 
        min = 0.0,
2932
 
        max = 100.0,
2933
 
        precision = 1,
2934
 
        subtype = 'PERCENTAGE')
2935
 
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
2936
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
2937
 
            ("linear", "Linear", "Simple and fast linear algorithm")),
2938
 
        description = "Algorithm used for interpolation",
2939
 
        default = 'cubic')
2940
 
    regular = bpy.props.BoolProperty(name = "Regular",
2941
 
        description = "Distribute vertices at constant distances along the" \
2942
 
            "curve",
2943
 
        default = True)
2944
 
    restriction = bpy.props.EnumProperty(name = "Restriction",
2945
 
        items = (("none", "None", "No restrictions on vertex movement"),
2946
 
            ("extrude", "Extrude only","Only allow extrusions (no "\
2947
 
                "indentations)"),
2948
 
            ("indent", "Indent only", "Only allow indentation (no "\
2949
 
                "extrusions)")),
2950
 
        description = "Restrictions on how the vertices can be moved",
2951
 
        default = 'none')
2952
 
    
2953
 
    @classmethod
2954
 
    def poll(cls, context):
2955
 
        ob = context.active_object
2956
 
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
2957
 
    
2958
 
    def draw(self, context):
2959
 
        layout = self.layout
2960
 
        col = layout.column()
2961
 
        
2962
 
        col.prop(self, "interpolation")
2963
 
        col.prop(self, "restriction")
2964
 
        col.prop(self, "boundaries")
2965
 
        col.prop(self, "regular")
2966
 
        col.separator()
2967
 
        
2968
 
        col.prop(self, "influence")
2969
 
    
2970
 
    def invoke(self, context, event):
2971
 
        # load custom settings
2972
 
        settings_load(self)
2973
 
        return self.execute(context)
2974
 
    
2975
 
    def execute(self, context):
2976
 
        # initialise
2977
 
        global_undo, object, mesh = initialise()
2978
 
        settings_write(self)
2979
 
        # check cache to see if we can save time
2980
 
        cached, single_loops, loops, derived, mapping = cache_read("Curve",
2981
 
            object, mesh, False, self.boundaries)
2982
 
        if cached:
2983
 
            derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
2984
 
        else:
2985
 
            # find loops
2986
 
            derived, mesh_mod, loops = curve_get_input(object, mesh,
2987
 
                self.boundaries, context.scene)
2988
 
            mapping = get_mapping(derived, mesh, mesh_mod, False, True, loops)
2989
 
            loops = check_loops(loops, mapping, mesh_mod)
2990
 
        verts_selected = [v.index for v in mesh_mod.vertices if v.select \
2991
 
            and not v.hide]
2992
 
        
2993
 
        # saving cache for faster execution next time
2994
 
        if not cached:
2995
 
            cache_write("Curve", object, mesh, False, self.boundaries, False,
2996
 
                loops, derived, mapping)
2997
 
        
2998
 
        move = []
2999
 
        for loop in loops:
3000
 
            knots, points = curve_calculate_knots(loop, verts_selected)
3001
 
            pknots = curve_project_knots(mesh_mod, verts_selected, knots,
3002
 
                points, loop[1])
3003
 
            tknots, tpoints = curve_calculate_t(mesh_mod, knots, points,
3004
 
                pknots, self.regular, loop[1])
3005
 
            splines = calculate_splines(self.interpolation, mesh_mod,
3006
 
                tknots, knots)
3007
 
            move.append(curve_calculate_vertices(mesh_mod, knots, tknots,
3008
 
                points, tpoints, splines, self.interpolation,
3009
 
                self.restriction))
3010
 
        
3011
 
        # move vertices to new locations
3012
 
        move_verts(mesh, mapping, move, self.influence)
3013
 
        
3014
 
        # cleaning up 
3015
 
        if derived:
3016
 
            bpy.context.blend_data.meshes.remove(mesh_mod)
3017
 
        
3018
 
        terminate(global_undo)
3019
 
        return{'FINISHED'}
3020
 
 
3021
 
 
3022
 
# flatten operator
3023
 
class Flatten(bpy.types.Operator):
3024
 
    bl_idname = "mesh.looptools_flatten"
3025
 
    bl_label = "Flatten"
3026
 
    bl_description = "Flatten vertices on a best-fitting plane"
3027
 
    bl_options = {'REGISTER', 'UNDO'}
3028
 
    
3029
 
    influence = bpy.props.FloatProperty(name = "Influence",
3030
 
        description = "Force of the tool",
3031
 
        default = 100.0,
3032
 
        min = 0.0,
3033
 
        max = 100.0,
3034
 
        precision = 1,
3035
 
        subtype = 'PERCENTAGE')
3036
 
    plane = bpy.props.EnumProperty(name = "Plane",
3037
 
        items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3038
 
            ("normal", "Normal", "Derive plane from averaging vertex "\
3039
 
            "normals"),
3040
 
            ("view", "View", "Flatten on a plane perpendicular to the "\
3041
 
            "viewing angle")),
3042
 
        description = "Plane on which vertices are flattened",
3043
 
        default = 'best_fit')
3044
 
    restriction = bpy.props.EnumProperty(name = "Restriction",
3045
 
        items = (("none", "None", "No restrictions on vertex movement"),
3046
 
            ("bounding_box", "Bounding box", "Vertices are restricted to "\
3047
 
            "movement inside the bounding box of the selection")),
3048
 
        description = "Restrictions on how the vertices can be moved",
3049
 
        default = 'none')
3050
 
    
3051
 
    @classmethod
3052
 
    def poll(cls, context):
3053
 
        ob = context.active_object
3054
 
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3055
 
    
3056
 
    def draw(self, context):
3057
 
        layout = self.layout
3058
 
        col = layout.column()
3059
 
        
3060
 
        col.prop(self, "plane")
3061
 
        #col.prop(self, "restriction")
3062
 
        col.separator()
3063
 
        
3064
 
        col.prop(self, "influence")
3065
 
    
3066
 
    def invoke(self, context, event):
3067
 
        # load custom settings
3068
 
        settings_load(self)
3069
 
        return self.execute(context)
3070
 
    
3071
 
    def execute(self, context):
3072
 
        # initialise
3073
 
        global_undo, object, mesh = initialise()
3074
 
        settings_write(self)
3075
 
        # check cache to see if we can save time
3076
 
        cached, single_loops, loops, derived, mapping = cache_read("Flatten",
3077
 
            object, mesh, False, False)
3078
 
        if not cached:
3079
 
            # order input into virtual loops
3080
 
            loops = flatten_get_input(mesh)
3081
 
            loops = check_loops(loops, mapping, mesh)
3082
 
        
3083
 
        # saving cache for faster execution next time
3084
 
        if not cached:
3085
 
            cache_write("Flatten", object, mesh, False, False, False, loops,
3086
 
                False, False)
3087
 
        
3088
 
        move = []
3089
 
        for loop in loops:
3090
 
            # calculate plane and position of vertices on them
3091
 
            com, normal = calculate_plane(mesh, loop, method=self.plane,
3092
 
                object=object)
3093
 
            to_move = flatten_project(mesh, loop, com, normal)
3094
 
            if self.restriction == 'none':
3095
 
                move.append(to_move)
3096
 
            else:
3097
 
                move.append(to_move)
3098
 
        move_verts(mesh, False, move, self.influence)
3099
 
        
3100
 
        terminate(global_undo)
3101
 
        return{'FINISHED'}
3102
 
 
3103
 
 
3104
 
# relax operator
3105
 
class Relax(bpy.types.Operator):
3106
 
    bl_idname = "mesh.looptools_relax"
3107
 
    bl_label = "Relax"
3108
 
    bl_description = "Relax the loop, so it is smoother"
3109
 
    bl_options = {'REGISTER', 'UNDO'}
3110
 
    
3111
 
    input = bpy.props.EnumProperty(name = "Input",
3112
 
        items = (("all", "Parallel (all)", "Also use non-selected "\
3113
 
                "parallel loops as input"),
3114
 
            ("selected", "Selection","Only use selected vertices as input")),
3115
 
        description = "Loops that are relaxed",
3116
 
        default = 'selected')
3117
 
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
3118
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3119
 
            ("linear", "Linear", "Simple and fast linear algorithm")),
3120
 
        description = "Algorithm used for interpolation",
3121
 
        default = 'cubic')
3122
 
    iterations = bpy.props.EnumProperty(name = "Iterations",
3123
 
        items = (("1", "1", "One"),
3124
 
            ("3", "3", "Three"),
3125
 
            ("5", "5", "Five"),
3126
 
            ("10", "10", "Ten"),
3127
 
            ("25", "25", "Twenty-five")),
3128
 
        description = "Number of times the loop is relaxed",
3129
 
        default = "1")
3130
 
    regular = bpy.props.BoolProperty(name = "Regular",
3131
 
        description = "Distribute vertices at constant distances along the" \
3132
 
            "loop",
3133
 
        default = True)
3134
 
    
3135
 
    @classmethod
3136
 
    def poll(cls, context):
3137
 
        ob = context.active_object
3138
 
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3139
 
    
3140
 
    def draw(self, context):
3141
 
        layout = self.layout
3142
 
        col = layout.column()
3143
 
        
3144
 
        col.prop(self, "interpolation")
3145
 
        col.prop(self, "input")
3146
 
        col.prop(self, "iterations")
3147
 
        col.prop(self, "regular")
3148
 
    
3149
 
    def invoke(self, context, event):
3150
 
        # load custom settings
3151
 
        settings_load(self)
3152
 
        return self.execute(context)
3153
 
    
3154
 
    def execute(self, context):
3155
 
        # initialise
3156
 
        global_undo, object, mesh = initialise()
3157
 
        settings_write(self)
3158
 
        # check cache to see if we can save time
3159
 
        cached, single_loops, loops, derived, mapping = cache_read("Relax",
3160
 
            object, mesh, self.input, False)
3161
 
        if cached:
3162
 
            derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
3163
 
        else:
3164
 
            # find loops
3165
 
            derived, mesh_mod, loops = get_connected_input(object, mesh,
3166
 
                context.scene, self.input)
3167
 
            mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops)
3168
 
            loops = check_loops(loops, mapping, mesh_mod)
3169
 
        knots, points = relax_calculate_knots(loops)
3170
 
        
3171
 
        # saving cache for faster execution next time
3172
 
        if not cached:
3173
 
            cache_write("Relax", object, mesh, self.input, False, False, loops,
3174
 
                derived, mapping)
3175
 
        
3176
 
        for iteration in range(int(self.iterations)):
3177
 
            # calculate splines and new positions
3178
 
            tknots, tpoints = relax_calculate_t(mesh_mod, knots, points,
3179
 
                self.regular)
3180
 
            splines = []
3181
 
            for i in range(len(knots)):
3182
 
                splines.append(calculate_splines(self.interpolation, mesh_mod,
3183
 
                    tknots[i], knots[i]))
3184
 
            move = [relax_calculate_verts(mesh_mod, self.interpolation,
3185
 
                tknots, knots, tpoints, points, splines)]
3186
 
            move_verts(mesh, mapping, move, -1)
3187
 
        
3188
 
        # cleaning up 
3189
 
        if derived:
3190
 
            bpy.context.blend_data.meshes.remove(mesh_mod)
3191
 
        terminate(global_undo)
3192
 
        
3193
 
        return{'FINISHED'}
3194
 
 
3195
 
 
3196
 
# space operator
3197
 
class Space(bpy.types.Operator):
3198
 
    bl_idname = "mesh.looptools_space"
3199
 
    bl_label = "Space"
3200
 
    bl_description = "Space the vertices in a regular distrubtion on the loop"
3201
 
    bl_options = {'REGISTER', 'UNDO'}
3202
 
    
3203
 
    influence = bpy.props.FloatProperty(name = "Influence",
3204
 
        description = "Force of the tool",
3205
 
        default = 100.0,
3206
 
        min = 0.0,
3207
 
        max = 100.0,
3208
 
        precision = 1,
3209
 
        subtype = 'PERCENTAGE')
3210
 
    input = bpy.props.EnumProperty(name = "Input",
3211
 
        items = (("all", "Parallel (all)", "Also use non-selected "\
3212
 
                "parallel loops as input"),
3213
 
            ("selected", "Selection","Only use selected vertices as input")),
3214
 
        description = "Loops that are spaced",
3215
 
        default = 'selected')
3216
 
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
3217
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3218
 
            ("linear", "Linear", "Vertices are projected on existing edges")),
3219
 
        description = "Algorithm used for interpolation",
3220
 
        default = 'cubic')
3221
 
    
3222
 
    @classmethod
3223
 
    def poll(cls, context):
3224
 
        ob = context.active_object
3225
 
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
3226
 
    
3227
 
    def draw(self, context):
3228
 
        layout = self.layout
3229
 
        col = layout.column()
3230
 
        
3231
 
        col.prop(self, "interpolation")
3232
 
        col.prop(self, "input")
3233
 
        col.separator()
3234
 
        
3235
 
        col.prop(self, "influence")
3236
 
    
3237
 
    def invoke(self, context, event):
3238
 
        # load custom settings
3239
 
        settings_load(self)
3240
 
        return self.execute(context)
3241
 
    
3242
 
    def execute(self, context):
3243
 
        # initialise
3244
 
        global_undo, object, mesh = initialise()
3245
 
        settings_write(self)
3246
 
        # check cache to see if we can save time
3247
 
        cached, single_loops, loops, derived, mapping = cache_read("Space",
3248
 
            object, mesh, self.input, False)
3249
 
        if cached:
3250
 
            derived, mesh_mod = get_derived_mesh(object, mesh, context.scene)
3251
 
        else:
3252
 
            # find loops
3253
 
            derived, mesh_mod, loops = get_connected_input(object, mesh,
3254
 
                context.scene, self.input)
3255
 
            mapping = get_mapping(derived, mesh, mesh_mod, False, False, loops)
3256
 
            loops = check_loops(loops, mapping, mesh_mod)
3257
 
        
3258
 
        # saving cache for faster execution next time
3259
 
        if not cached:
3260
 
            cache_write("Space", object, mesh, self.input, False, False, loops,
3261
 
                derived, mapping)
3262
 
        
3263
 
        move = []
3264
 
        for loop in loops:
3265
 
            # calculate splines and new positions
3266
 
            if loop[1]: # circular
3267
 
                loop[0].append(loop[0][0])
3268
 
            tknots, tpoints = space_calculate_t(mesh_mod, loop[0][:])
3269
 
            splines = calculate_splines(self.interpolation, mesh_mod,
3270
 
                tknots, loop[0][:])
3271
 
            move.append(space_calculate_verts(mesh_mod, self.interpolation,
3272
 
                tknots, tpoints, loop[0][:-1], splines))
3273
 
        
3274
 
        # move vertices to new locations
3275
 
        move_verts(mesh, mapping, move, self.influence)
3276
 
        
3277
 
        # cleaning up 
3278
 
        if derived:
3279
 
            bpy.context.blend_data.meshes.remove(mesh_mod)
3280
 
        terminate(global_undo)
3281
 
        
3282
 
        return{'FINISHED'}
3283
 
 
3284
 
 
3285
 
##########################################
3286
 
####### GUI and registration #############
3287
 
##########################################
3288
 
 
3289
 
# menu containing all tools
3290
 
class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
3291
 
    bl_label = "LoopTools"
3292
 
    
3293
 
    def draw(self, context):
3294
 
        layout = self.layout
3295
 
        
3296
 
#        layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
3297
 
        layout.operator("mesh.looptools_circle")
3298
 
        layout.operator("mesh.looptools_curve")
3299
 
        layout.operator("mesh.looptools_flatten")
3300
 
#        layout.operator("mesh.looptools_bridge", text="Loft").loft = True
3301
 
        layout.operator("mesh.looptools_relax")
3302
 
        layout.operator("mesh.looptools_space")
3303
 
 
3304
 
 
3305
 
# panel containing all tools
3306
 
class VIEW3D_PT_tools_looptools(bpy.types.Panel):
3307
 
    bl_space_type = 'VIEW_3D'
3308
 
    bl_region_type = 'TOOLS'
3309
 
    bl_context = "mesh_edit"
3310
 
    bl_label = "LoopTools"
3311
 
 
3312
 
    def draw(self, context):
3313
 
        layout = self.layout
3314
 
        col = layout.column(align=True)
3315
 
        lt = context.window_manager.looptools
3316
 
        
3317
 
        # bridge - first line
3318
 
#        split = col.split(percentage=0.15)
3319
 
#        if lt.display_bridge:
3320
 
#            split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
3321
 
#        else:
3322
 
#            split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
3323
 
#        split.operator("mesh.looptools_bridge", text="Bridge").loft = False
3324
 
        # bridge - settings
3325
 
#        if lt.display_bridge:
3326
 
#            box = col.column(align=True).box().column()
3327
 
            #box.prop(self, "mode")
3328
 
            
3329
 
            # top row
3330
 
#            col_top = box.column(align=True)
3331
 
#            row = col_top.row(align=True)
3332
 
#            col_left = row.column(align=True)
3333
 
#            col_right = row.column(align=True)
3334
 
#            col_right.active = lt.bridge_segments != 1
3335
 
#            col_left.prop(lt, "bridge_segments")
3336
 
#            col_right.prop(lt, "bridge_min_width", text="")
3337
 
#            # bottom row
3338
 
#            bottom_left = col_left.row()
3339
 
#            bottom_left.active = lt.bridge_segments != 1
3340
 
#            bottom_left.prop(lt, "bridge_interpolation", text="")
3341
 
#            bottom_right = col_right.row()
3342
 
#            bottom_right.active = lt.bridge_interpolation == 'cubic'
3343
 
#            bottom_right.prop(lt, "bridge_cubic_strength")
3344
 
            # boolean properties
3345
 
#            col_top.prop(lt, "bridge_remove_faces")
3346
 
            
3347
 
            # override properties
3348
 
#            col_top.separator()
3349
 
#            row = box.row(align = True)
3350
 
#            row.prop(lt, "bridge_twist")
3351
 
#            row.prop(lt, "bridge_reverse")
3352
 
        
3353
 
        # circle - first line
3354
 
        split = col.split(percentage=0.15)
3355
 
        if lt.display_circle:
3356
 
            split.prop(lt, "display_circle", text="", icon='DOWNARROW_HLT')
3357
 
        else:
3358
 
            split.prop(lt, "display_circle", text="", icon='RIGHTARROW')
3359
 
        split.operator("mesh.looptools_circle")
3360
 
        # circle - settings
3361
 
        if lt.display_circle:
3362
 
            box = col.column(align=True).box().column()
3363
 
            box.prop(lt, "circle_fit")
3364
 
            box.separator()
3365
 
            
3366
 
            box.prop(lt, "circle_flatten")
3367
 
            row = box.row(align=True)
3368
 
            row.prop(lt, "circle_custom_radius")
3369
 
            row_right = row.row(align=True)
3370
 
            row_right.active = lt.circle_custom_radius
3371
 
            row_right.prop(lt, "circle_radius", text="")
3372
 
            box.prop(lt, "circle_regular")
3373
 
            box.separator()
3374
 
            
3375
 
            box.prop(lt, "circle_influence")
3376
 
        
3377
 
        # curve - first line
3378
 
        split = col.split(percentage=0.15)
3379
 
        if lt.display_curve:
3380
 
            split.prop(lt, "display_curve", text="", icon='DOWNARROW_HLT')
3381
 
        else:
3382
 
            split.prop(lt, "display_curve", text="", icon='RIGHTARROW')
3383
 
        split.operator("mesh.looptools_curve")
3384
 
        # curve - settings
3385
 
        if lt.display_curve:
3386
 
            box = col.column(align=True).box().column()
3387
 
            box.prop(lt, "curve_interpolation")
3388
 
            box.prop(lt, "curve_restriction")
3389
 
            box.prop(lt, "curve_boundaries")
3390
 
            box.prop(lt, "curve_regular")
3391
 
            box.separator()
3392
 
            
3393
 
            box.prop(lt, "curve_influence")
3394
 
        
3395
 
        # flatten - first line
3396
 
        split = col.split(percentage=0.15)
3397
 
        if lt.display_flatten:
3398
 
            split.prop(lt, "display_flatten", text="", icon='DOWNARROW_HLT')
3399
 
        else:
3400
 
            split.prop(lt, "display_flatten", text="", icon='RIGHTARROW')
3401
 
        split.operator("mesh.looptools_flatten")
3402
 
        # flatten - settings
3403
 
        if lt.display_flatten:
3404
 
            box = col.column(align=True).box().column()
3405
 
            box.prop(lt, "flatten_plane")
3406
 
            #box.prop(lt, "flatten_restriction")
3407
 
            box.separator()
3408
 
            
3409
 
            box.prop(lt, "flatten_influence")
3410
 
        
3411
 
        # loft - first line
3412
 
#        split = col.split(percentage=0.15)
3413
 
#        if lt.display_loft:
3414
 
#            split.prop(lt, "display_loft", text="", icon='DOWNARROW_HLT')
3415
 
#        else:
3416
 
#            split.prop(lt, "display_loft", text="", icon='RIGHTARROW')
3417
 
#        split.operator("mesh.looptools_bridge", text="Loft").loft = True
3418
 
#        # loft - settings
3419
 
#        if lt.display_loft:
3420
 
#            box = col.column(align=True).box().column()
3421
 
#            #box.prop(self, "mode")
3422
 
#            
3423
 
#            # top row
3424
 
#            col_top = box.column(align=True)
3425
 
#            row = col_top.row(align=True)
3426
 
#            col_left = row.column(align=True)
3427
 
#            col_right = row.column(align=True)
3428
 
#            col_right.active = lt.bridge_segments != 1
3429
 
#            col_left.prop(lt, "bridge_segments")
3430
 
#            col_right.prop(lt, "bridge_min_width", text="")
3431
 
#            # bottom row
3432
 
#            bottom_left = col_left.row()
3433
 
#            bottom_left.active = lt.bridge_segments != 1
3434
 
#            bottom_left.prop(lt, "bridge_interpolation", text="")
3435
 
#            bottom_right = col_right.row()
3436
 
#            bottom_right.active = lt.bridge_interpolation == 'cubic'
3437
 
#            bottom_right.prop(lt, "bridge_cubic_strength")
3438
 
#            # boolean properties
3439
 
#            col_top.prop(lt, "bridge_remove_faces")
3440
 
#            col_top.prop(lt, "bridge_loft_loop")
3441
 
#            
3442
 
#            # override properties
3443
 
#            col_top.separator()
3444
 
#            row = box.row(align = True)
3445
 
#            row.prop(lt, "bridge_twist")
3446
 
#            row.prop(lt, "bridge_reverse")
3447
 
        
3448
 
        # relax - first line
3449
 
        split = col.split(percentage=0.15)
3450
 
        if lt.display_relax:
3451
 
            split.prop(lt, "display_relax", text="", icon='DOWNARROW_HLT')
3452
 
        else:
3453
 
            split.prop(lt, "display_relax", text="", icon='RIGHTARROW')
3454
 
        split.operator("mesh.looptools_relax")
3455
 
        # relax - settings
3456
 
        if lt.display_relax:
3457
 
            box = col.column(align=True).box().column()
3458
 
            box.prop(lt, "relax_interpolation")
3459
 
            box.prop(lt, "relax_input")
3460
 
            box.prop(lt, "relax_iterations")
3461
 
            box.prop(lt, "relax_regular")
3462
 
        
3463
 
        # space - first line
3464
 
        split = col.split(percentage=0.15)
3465
 
        if lt.display_space:
3466
 
            split.prop(lt, "display_space", text="", icon='DOWNARROW_HLT')
3467
 
        else:
3468
 
            split.prop(lt, "display_space", text="", icon='RIGHTARROW')
3469
 
        split.operator("mesh.looptools_space")
3470
 
        # space - settings
3471
 
        if lt.display_space:
3472
 
            box = col.column(align=True).box().column()
3473
 
            box.prop(lt, "space_interpolation")
3474
 
            box.prop(lt, "space_input")
3475
 
            box.separator()
3476
 
            
3477
 
            box.prop(lt, "space_influence")
3478
 
 
3479
 
 
3480
 
# property group containing all properties for the gui in the panel
3481
 
class LoopToolsProps(bpy.types.PropertyGroup):
3482
 
    """
3483
 
    Fake module like class
3484
 
    bpy.context.window_manager.looptools
3485
 
    """
3486
 
    
3487
 
    # general display properties
3488
 
#    display_bridge = bpy.props.BoolProperty(name = "Bridge settings",
3489
 
#        description = "Display settings of the Bridge tool",
3490
 
#        default = False)
3491
 
    display_circle = bpy.props.BoolProperty(name = "Circle settings",
3492
 
        description = "Display settings of the Circle tool",
3493
 
        default = False)
3494
 
    display_curve = bpy.props.BoolProperty(name = "Curve settings",
3495
 
        description = "Display settings of the Curve tool",
3496
 
        default = False)
3497
 
    display_flatten = bpy.props.BoolProperty(name = "Flatten settings",
3498
 
        description = "Display settings of the Flatten tool",
3499
 
        default = False)
3500
 
#    display_loft = bpy.props.BoolProperty(name = "Loft settings",
3501
 
#        description = "Display settings of the Loft tool",
3502
 
#        default = False)
3503
 
    display_relax = bpy.props.BoolProperty(name = "Relax settings",
3504
 
        description = "Display settings of the Relax tool",
3505
 
        default = False)
3506
 
    display_space = bpy.props.BoolProperty(name = "Space settings",
3507
 
        description = "Display settings of the Space tool",
3508
 
        default = False)
3509
 
    
3510
 
    # bridge properties
3511
 
    bridge_cubic_strength = bpy.props.FloatProperty(name = "Strength",
3512
 
        description = "Higher strength results in more fluid curves",
3513
 
        default = 1.0,
3514
 
        soft_min = -3.0,
3515
 
        soft_max = 3.0)
3516
 
    bridge_interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
3517
 
        items = (('cubic', "Cubic", "Gives curved results"),
3518
 
            ('linear', "Linear", "Basic, fast, straight interpolation")),
3519
 
        description = "Interpolation mode: algorithm used when creating "\
3520
 
            "segments",
3521
 
        default = 'cubic')
3522
 
    bridge_loft = bpy.props.BoolProperty(name = "Loft",
3523
 
        description = "Loft multiple loops, instead of considering them as "\
3524
 
            "a multi-input for bridging",
3525
 
        default = False)
3526
 
    bridge_loft_loop = bpy.props.BoolProperty(name = "Loop",
3527
 
        description = "Connect the first and the last loop with each other",
3528
 
        default = False)
3529
 
    bridge_min_width = bpy.props.IntProperty(name = "Minimum width",
3530
 
        description = "Segments with an edge smaller than this are merged "\
3531
 
            "(compared to base edge)",
3532
 
        default = 0,
3533
 
        min = 0,
3534
 
        max = 100,
3535
 
        subtype = 'PERCENTAGE')
3536
 
    bridge_mode = bpy.props.EnumProperty(name = "Mode",
3537
 
        items = (('basic', "Basic", "Fast algorithm"),
3538
 
                 ('shortest', "Shortest edge", "Slower algorithm with " \
3539
 
                                               "better vertex matching")),
3540
 
        description = "Algorithm used for bridging",
3541
 
        default = 'shortest')
3542
 
    bridge_remove_faces = bpy.props.BoolProperty(name = "Remove faces",
3543
 
        description = "Remove faces that are internal after bridging",
3544
 
        default = True)
3545
 
    bridge_reverse = bpy.props.BoolProperty(name = "Reverse",
3546
 
        description = "Manually override the direction in which the loops "\
3547
 
                      "are bridged. Only use if the tool gives the wrong " \
3548
 
                      "result",
3549
 
        default = False)
3550
 
    bridge_segments = bpy.props.IntProperty(name = "Segments",
3551
 
        description = "Number of segments used to bridge the gap "\
3552
 
            "(0 = automatic)",
3553
 
        default = 1,
3554
 
        min = 0,
3555
 
        soft_max = 20)
3556
 
    bridge_twist = bpy.props.IntProperty(name = "Twist",
3557
 
        description = "Twist what vertices are connected to each other",
3558
 
        default = 0)
3559
 
    
3560
 
    # circle properties
3561
 
    circle_custom_radius = bpy.props.BoolProperty(name = "Radius",
3562
 
        description = "Force a custom radius",
3563
 
        default = False)
3564
 
    circle_fit = bpy.props.EnumProperty(name = "Method",
3565
 
        items = (("best", "Best fit", "Non-linear least squares"),
3566
 
            ("inside", "Fit inside","Only move vertices towards the center")),
3567
 
        description = "Method used for fitting a circle to the vertices",
3568
 
        default = 'best')
3569
 
    circle_flatten = bpy.props.BoolProperty(name = "Flatten",
3570
 
        description = "Flatten the circle, instead of projecting it on the " \
3571
 
            "mesh",
3572
 
        default = True)
3573
 
    circle_influence = bpy.props.FloatProperty(name = "Influence",
3574
 
        description = "Force of the tool",
3575
 
        default = 100.0,
3576
 
        min = 0.0,
3577
 
        max = 100.0,
3578
 
        precision = 1,
3579
 
        subtype = 'PERCENTAGE')
3580
 
    circle_radius = bpy.props.FloatProperty(name = "Radius",
3581
 
        description = "Custom radius for circle",
3582
 
        default = 1.0,
3583
 
        min = 0.0,
3584
 
        soft_max = 1000.0)
3585
 
    circle_regular = bpy.props.BoolProperty(name = "Regular",
3586
 
        description = "Distribute vertices at constant distances along the " \
3587
 
            "circle",
3588
 
        default = True)
3589
 
    
3590
 
    # curve properties
3591
 
    curve_boundaries = bpy.props.BoolProperty(name = "Boundaries",
3592
 
        description = "Limit the tool to work within the boundaries of the "\
3593
 
            "selected vertices",
3594
 
        default = False)
3595
 
    curve_influence = bpy.props.FloatProperty(name = "Influence",
3596
 
        description = "Force of the tool",
3597
 
        default = 100.0,
3598
 
        min = 0.0,
3599
 
        max = 100.0,
3600
 
        precision = 1,
3601
 
        subtype = 'PERCENTAGE')
3602
 
    curve_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3603
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3604
 
            ("linear", "Linear", "Simple and fast linear algorithm")),
3605
 
        description = "Algorithm used for interpolation",
3606
 
        default = 'cubic')
3607
 
    curve_regular = bpy.props.BoolProperty(name = "Regular",
3608
 
        description = "Distribute vertices at constant distances along the" \
3609
 
            "curve",
3610
 
        default = True)
3611
 
    curve_restriction = bpy.props.EnumProperty(name = "Restriction",
3612
 
        items = (("none", "None", "No restrictions on vertex movement"),
3613
 
            ("extrude", "Extrude only","Only allow extrusions (no "\
3614
 
                "indentations)"),
3615
 
            ("indent", "Indent only", "Only allow indentation (no "\
3616
 
                "extrusions)")),
3617
 
        description = "Restrictions on how the vertices can be moved",
3618
 
        default = 'none')
3619
 
    
3620
 
    # flatten properties
3621
 
    flatten_influence = bpy.props.FloatProperty(name = "Influence",
3622
 
        description = "Force of the tool",
3623
 
        default = 100.0,
3624
 
        min = 0.0,
3625
 
        max = 100.0,
3626
 
        precision = 1,
3627
 
        subtype = 'PERCENTAGE')
3628
 
    flatten_plane = bpy.props.EnumProperty(name = "Plane",
3629
 
        items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
3630
 
            ("normal", "Normal", "Derive plane from averaging vertex "\
3631
 
            "normals"),
3632
 
            ("view", "View", "Flatten on a plane perpendicular to the "\
3633
 
            "viewing angle")),
3634
 
        description = "Plane on which vertices are flattened",
3635
 
        default = 'best_fit')
3636
 
    flatten_restriction = bpy.props.EnumProperty(name = "Restriction",
3637
 
        items = (("none", "None", "No restrictions on vertex movement"),
3638
 
            ("bounding_box", "Bounding box", "Vertices are restricted to "\
3639
 
            "movement inside the bounding box of the selection")),
3640
 
        description = "Restrictions on how the vertices can be moved",
3641
 
        default = 'none')
3642
 
    
3643
 
    # relax properties
3644
 
    relax_input = bpy.props.EnumProperty(name = "Input",
3645
 
        items = (("all", "Parallel (all)", "Also use non-selected "\
3646
 
                "parallel loops as input"),
3647
 
            ("selected", "Selection","Only use selected vertices as input")),
3648
 
        description = "Loops that are relaxed",
3649
 
        default = 'selected')
3650
 
    relax_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3651
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3652
 
            ("linear", "Linear", "Simple and fast linear algorithm")),
3653
 
        description = "Algorithm used for interpolation",
3654
 
        default = 'cubic')
3655
 
    relax_iterations = bpy.props.EnumProperty(name = "Iterations",
3656
 
        items = (("1", "1", "One"),
3657
 
            ("3", "3", "Three"),
3658
 
            ("5", "5", "Five"),
3659
 
            ("10", "10", "Ten"),
3660
 
            ("25", "25", "Twenty-five")),
3661
 
        description = "Number of times the loop is relaxed",
3662
 
        default = "1")
3663
 
    relax_regular = bpy.props.BoolProperty(name = "Regular",
3664
 
        description = "Distribute vertices at constant distances along the" \
3665
 
            "loop",
3666
 
        default = True)
3667
 
    
3668
 
    # space properties
3669
 
    space_influence = bpy.props.FloatProperty(name = "Influence",
3670
 
        description = "Force of the tool",
3671
 
        default = 100.0,
3672
 
        min = 0.0,
3673
 
        max = 100.0,
3674
 
        precision = 1,
3675
 
        subtype = 'PERCENTAGE')
3676
 
    space_input = bpy.props.EnumProperty(name = "Input",
3677
 
        items = (("all", "Parallel (all)", "Also use non-selected "\
3678
 
                "parallel loops as input"),
3679
 
            ("selected", "Selection","Only use selected vertices as input")),
3680
 
        description = "Loops that are spaced",
3681
 
        default = 'selected')
3682
 
    space_interpolation = bpy.props.EnumProperty(name = "Interpolation",
3683
 
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
3684
 
            ("linear", "Linear", "Vertices are projected on existing edges")),
3685
 
        description = "Algorithm used for interpolation",
3686
 
        default = 'cubic')
3687
 
 
3688
 
 
3689
 
# draw function for integration in menus
3690
 
def menu_func(self, context):
3691
 
    self.layout.menu("VIEW3D_MT_edit_mesh_looptools")
3692
 
    self.layout.separator()
3693
 
 
3694
 
 
3695
 
# define classes for registration
3696
 
classes = [VIEW3D_MT_edit_mesh_looptools,
3697
 
    VIEW3D_PT_tools_looptools,
3698
 
    LoopToolsProps,
3699
 
    Bridge,
3700
 
    Circle,
3701
 
    Curve,
3702
 
    Flatten,
3703
 
    Relax,
3704
 
    Space]
3705
 
 
3706
 
 
3707
 
# registering and menu integration
3708
 
def register():
3709
 
    for c in classes:
3710
 
        bpy.utils.register_class(c)
3711
 
    bpy.types.VIEW3D_MT_edit_mesh_specials.prepend(menu_func)
3712
 
    bpy.types.WindowManager.looptools = bpy.props.PointerProperty(\
3713
 
        type = LoopToolsProps)
3714
 
 
3715
 
 
3716
 
# unregistering and removing menus
3717
 
def unregister():
3718
 
    for c in classes:
3719
 
        bpy.utils.unregister_class(c)
3720
 
    bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
3721
 
    try:
3722
 
        del bpy.types.WindowManager.looptools
3723
 
    except:
3724
 
        pass
3725
 
 
3726
 
 
3727
 
if __name__ == "__main__":
3728
 
    register()