Fixed a minor typo (WIP)
[blender-addons-contrib.git] / curve_tools.py
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": "Curve Tools",
21     "author": "Zak",
22     "version": (0, 1, 5),
23     "blender": (2, 5, 9),
24     "location": "Properties > Object data",
25     "description": "Creates driven Lofts or Birails between curves",
26     "warning": "may be buggy or incomplete",
27     "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"\
28         "Scripts/Curve/Curve_Tools",
29     "tracker_url": "https://projects.blender.org/tracker/index.php?"\
30         "func=detail&aid=27720",
31     "category": "Add Curve"}
32
33 ### UPDATES
34 #1.5
35
36 #-Fixed birail function
37 #-Added Curve Snap to W key specials menu.
38 #-Removed some functions that arent needed and wrapped into the operators.
39 #-nurbs with weights for loft and birail
40 #-Panel Moved to view 3d tools
41 #-inserted TODO comments
42 #-tried to implement live tension and bias for Hermite interpolation by driving the mesh but
43 #i dont know why, the code is executed all the time even if you dont change the variables.
44 #-snap to curves affects all the curves on the scene
45 #-i was able to preserve handle types when split or subdivide
46
47
48 #1.4
49 #-incorporate curve snap
50 #assign a copy transform to helper
51 #-nurbs implemented (in progress)
52
53 import bpy
54 from mathutils import *
55 from bpy.props import *
56
57 print("----------")
58
59
60 ### PROPERTIES
61 class sprops(bpy.types.PropertyGroup):
62     pass
63
64
65 bpy.utils.register_class(sprops)
66
67 #bpy.selection will store objects names in the order they were selected
68 bpy.selection=[]
69
70
71 #dodriver a simple checker to chosse whether  you want a driven mesh or not.
72 bpy.types.Scene.dodriver = BoolProperty(name = "dodriver",                                      default=False)
73
74 #interpolation types
75 myitems = (('0','Linear', ''),('1','Cubic',''),('2','Catmull',''), ('3','Hermite',''))
76 bpy.types.Scene.intype = EnumProperty(name="intype", items = myitems, default='3')
77
78 #number of steps and spans to be created
79 bpy.types.Scene.steps = IntProperty(name="steps", default=12, min=2)
80 bpy.types.Scene.spans = IntProperty(name="spans", default=12, min=2)
81
82 #parameters for Hermite interpolation
83 bpy.types.Scene.tension = FloatProperty(name = "tension", min=0.0, default=0.0)
84 bpy.types.Scene.bias = FloatProperty(name = "bias", min=0.0, default = 0.5)
85
86 #proportional birail
87 bpy.types.Scene.proportional = BoolProperty(name="proportional", default=False)
88
89 #this stores the result of calculating the curve length
90 bpy.types.Scene.clen = FloatProperty(name="clen", default=0.0, precision=5)
91
92 #minimun distance for merge curve tool
93 bpy.types.Scene.limit = FloatProperty(name="limit", default=0.1, precision=3)
94
95
96 ### SELECT BY ORDER BLOCK
97
98 #i dont know what to do with this. Im not using it yet.
99 def selected_points(curve):
100
101     selp = []
102     for spl in curve.splines:
103         if spl.type=="BEZIER":
104             points = spl.bezier_points
105             for p in points:
106                 if p.select_control_point:
107                     selp.append(p)
108
109         elif spl.type=="NURBS":
110             points = spl.points
111             for p in points:
112                 if p.select:
113                     selp.append(p)
114     return selp
115
116 #writes bpy.selection when a new object is selected or deselected
117 #it compares bpy.selection with bpy.context.selected_objects
118
119 def select():
120
121     #print(bpy.context.mode)
122     if bpy.context.mode=="OBJECT":
123         obj = bpy.context.object
124         sel = len(bpy.context.selected_objects)
125
126         if sel==0:
127             bpy.selection=[]
128         else:
129             if sel==1:
130                 bpy.selection=[]
131                 bpy.selection.append(obj)
132             elif sel>len(bpy.selection):
133                 for sobj in bpy.context.selected_objects:
134                     if (sobj in bpy.selection)==False:
135                         bpy.selection.append(sobj)
136
137             elif sel<len(bpy.selection):
138                 for it in bpy.selection:
139                     if (it in bpy.context.selected_objects)==False:
140                         bpy.selection.remove(it)
141
142     #on edit mode doesnt work well
143
144
145 #executes selection by order at 3d view
146 class Selection(bpy.types.Header):
147     bl_label = "Selection"
148     bl_space_type = "VIEW_3D"
149
150     def __init__(self):
151         #print("hey")
152         select()
153
154     def draw(self, context):
155         layout = self.layout
156         row = layout.row()
157         row.label("Sel: "+str(len(bpy.selection)))
158
159 ### GENERAL CURVE FUNCTIONS
160
161 #distance between 2 points
162 def dist(p1, p2):
163     return (p2-p1).magnitude
164
165 #sets cursors position for debugging porpuses
166 def cursor(pos):
167     bpy.context.scene.cursor_location = pos
168
169 #cuadratic bezier value
170 def quad(p, t):
171     return p[0]*(1.0-t)**2.0 + 2.0*t*p[1]*(1.0-t) + p[2]*t**2.0
172
173 #cubic bezier value
174 def cubic(p, t):
175     return p[0]*(1.0-t)**3.0 + 3.0*p[1]*t*(1.0-t)**2.0 + 3.0*p[2]*(t**2.0)*(1.0-t) + p[3]*t**3.0
176
177 #gets a bezier segment's control points on global coordinates
178 def getbezpoints(spl, mt, seg=0):
179     points = spl.bezier_points
180     p0 = mt * points[seg].co
181     p1 = mt * points[seg].handle_right
182     p2 = mt * points[seg+1].handle_left
183     p3 = mt * points[seg+1].co
184     return p0, p1, p2, p3
185
186 #gets nurbs polygon control points on global coordinates
187 def getnurbspoints(spl, mw):
188     pts = []
189     ws = []
190     for p in spl.points:
191         v = Vector(p.co[0:3])*mw
192         pts.append(v)
193         ws.append(p.weight)
194     return pts , ws
195
196 #calcs a nurbs knot vector
197 def knots(n, order, type=0):#0 uniform 1 endpoints 2 bezier
198
199     kv = []
200
201     t = n+order
202     if type==0:
203         for i in range(0, t):
204             kv.append(1.0*i)
205
206     elif type==1:
207         k=0.0
208         for i in range(1, t+1):
209             kv.append(k)
210             if i>=order and i<=n:
211                 k+=1.0
212     elif type==2:
213         if order==4:
214             k=0.34
215             for a in range(0,t):
216                 if a>=order and a<=n: k+=0.5
217                 kv.append(floor(k))
218                 k+=1.0/3.0
219
220         elif order==3:
221             k=0.6
222             for a in range(0, t):
223                 if a >=order and a<=n: k+=0.5
224                 kv.append(floor(k))
225
226     ##normalize the knot vector
227     for i in range(0, len(kv)):
228         kv[i]=kv[i]/kv[-1]
229
230     return kv
231
232 #nurbs curve evaluation
233 def C(t, order, points, weights, knots):
234     #c = Point([0,0,0])
235     c = Vector()
236     rational = 0
237     i = 0
238     while i < len(points):
239         b = B(i, order, t, knots)
240         p = points[i] * (b * weights[i])
241         c = c + p
242         rational = rational + b*weights[i]
243         i = i + 1
244
245     return c * (1.0/rational)
246
247 #nurbs basis function
248 def B(i,k,t,knots):
249     ret = 0
250     if k>0:
251         n1 = (t-knots[i])*B(i,k-1,t,knots)
252         d1 = knots[i+k] - knots[i]
253         n2 = (knots[i+k+1] - t) * B(i+1,k-1,t,knots)
254         d2 = knots[i+k+1] - knots[i+1]
255         if d1 > 0.0001 or d1 < -0.0001:
256             a = n1 / d1
257         else:
258             a = 0
259         if d2 > 0.0001 or d2 < -0.0001:
260             b = n2 / d2
261         else:
262             b = 0
263         ret = a + b
264         #print "B i = %d, k = %d, ret = %g, a = %g, b = %g\n"%(i,k,ret,a,b)
265     else:
266         if knots[i] <= t and t <= knots[i+1]:
267             ret = 1
268         else:
269             ret = 0
270     return ret
271
272 #calculates a global parameter t along all control points
273 #t=0 begining of the curve
274 #t=1 ending of the curve
275
276 def calct(obj, t):
277
278     spl=None
279     mw = obj.matrix_world
280     if obj.data.splines.active==None:
281         if len(obj.data.splines)>0:
282             spl=obj.data.splines[0]
283     else:
284         spl = obj.data.splines.active
285
286     if spl==None:
287         return False
288
289     if spl.type=="BEZIER":
290         points = spl.bezier_points
291         nsegs = len(points)-1
292
293         d = 1.0/nsegs
294         seg = int(t/d)
295         t1 = t/d - int(t/d)
296
297         if t==1:
298             seg-=1
299             t1 = 1.0
300
301         p = getbezpoints(spl,mw, seg)
302
303         coord = cubic(p, t1)
304
305         return coord
306
307     elif spl.type=="NURBS":
308         data = getnurbspoints(spl, mw)
309         pts = data[0]
310         ws = data[1]
311         order = spl.order_u
312         n = len(pts)
313         ctype = spl.use_endpoint_u
314         kv = knots(n, order, ctype)
315
316         coord = C(t, order-1, pts, ws, kv)
317
318         return coord
319
320 #length of the curve
321 def arclength(objs):
322     length = 0.0
323
324     for obj in objs:
325         if obj.type=="CURVE":
326             prec = 1000 #precision
327             inc = 1/prec #increments
328
329             ### TODO: set a custom precision value depending the number of curve points
330             #that way it can gain on accuracy in less operations.
331
332             #subdivide the curve in 1000 lines and sum its magnitudes
333             for i in range(0, prec):
334                 ti = i*inc
335                 tf = (i+1)*inc
336                 a = calct(obj, ti)
337                 b = calct(obj, tf)
338                 r = (b-a).magnitude
339                 length+=r
340
341     return length
342
343
344 class ArcLengthOperator(bpy.types.Operator):
345
346     bl_idname = "curve.arc_length_operator"
347     bl_label = "Measures the length of a curve"
348
349     @classmethod
350     def poll(cls, context):
351         return context.active_object != None
352
353     def execute(self, context):
354         objs = context.selected_objects
355         context.scene.clen = arclength(objs)
356         return {'FINISHED'}
357
358 ### LOFT INTERPOLATIONS
359
360 #objs = selected objects
361 #i = object index
362 #t = parameter along u direction
363 #tr = parameter along v direction
364
365 #linear
366 def intl(objs, i, t, tr):
367     p1 = calct(objs[i],t)
368     p2 = calct(objs[i+1], t)
369
370     r = p1 + (p2 - p1)*tr
371
372     return r
373
374 #tipo = interpolation type
375 #tension and bias are for hermite interpolation
376 #they can be changed to obtain different lofts.
377
378 #cubic
379 def intc(objs, i, t, tr, tipo=3, tension=0.0, bias=0.0):
380
381     ncurves =len(objs)
382
383     #if 2 curves go to linear interpolation regardless the one you choose
384     if ncurves<3:
385         return intl(objs, i, t, tr)
386     else:
387
388         #calculates the points to be interpolated on each curve
389         if i==0:
390             p0 = calct(objs[i], t)
391             p1 = p0
392             p2 = calct(objs[i+1], t)
393             p3 = calct(objs[i+2], t)
394         else:
395             if ncurves-2 == i:
396                 p0 = calct(objs[i-1], t)
397                 p1 = calct(objs[i], t)
398                 p2 = calct(objs[i+1], t)
399                 p3 = p2
400             else:
401                 p0 = calct(objs[i-1], t)
402                 p1 = calct(objs[i], t)
403                 p2 = calct(objs[i+1], t)
404                 p3 = calct(objs[i+2], t)
405
406
407     #calculates the interpolation between those points
408     #i used methods from this page: http://paulbourke.net/miscellaneous/interpolation/
409
410     if tipo==0:
411         #linear
412         return intl(objs, i, t, tr)
413     elif tipo == 1:
414         #natural cubic
415         t2 = tr*tr
416         a0 = p3-p2-p0+p1
417         a1 = p0-p1-a0
418         a2 = p2-p0
419         a3 = p1
420         return a0*tr*t2 + a1*t2+a2*tr+a3
421     elif tipo == 2:
422         #catmull it seems to be working. ill leave it for now.
423         t2 = tr*tr
424         a0 = -0.5*p0 +1.5*p1 -1.5*p2 +0.5*p3
425         a1 = p0 - 2.5*p1 + 2*p2 -0.5*p3
426         a2 = -0.5*p0 + 0.5 *p2
427         a3 = p1
428         return a0*tr*tr + a1*t2+a2*tr+a3
429
430     elif tipo == 3:
431         #hermite
432         tr2 = tr*tr
433         tr3 = tr2*tr
434         m0 = (p1-p0)*(1+bias)*(1-tension)/2
435         m0+= (p2-p1)*(1-bias)*(1-tension)/2
436         m1 = (p2-p1)*(1+bias)*(1-tension)/2
437         m1+= (p3-p2)*(1-bias)*(1-tension)/2
438         a0 = 2*tr3 - 3*tr2 + 1
439         a1 = tr3 - 2 * tr2+ tr
440         a2 = tr3 - tr2
441         a3 = -2*tr3 + 3*tr2
442
443         return a0*p1+a1*m0+a2*m1+a3*p2
444
445
446 #handles loft driver expression
447 #example: loftdriver('Loft', 'BezierCurve;BezierCurve.001;BezierCurve.002', 3)
448
449 #name: its the name of the mesh to be driven
450 #objs: the  names of the curves that drives the mesh
451 #3 interpolation type
452
453 def loftdriver(name, objs, intype):
454     #print("ejecutando "+name)
455     intype = int(intype)
456
457     tension = 0.0
458     bias = 0.5
459     #if the loft object still exists proceed normal
460     try:
461         resobj = bpy.data.objects[name]
462         spans = resobj["spans"]
463         steps = resobj["steps"]
464         if intype==3: #hermite
465             tension = resobj['tension']
466             bias = resobj['bias']
467
468     #if not delete the driver
469     except:
470         curve = bpy.context.object
471         for it in curve.keys():
472             if it == "driver":
473                 curve.driver_remove('["driver"]')
474         return False
475
476     objs = objs.split(";")
477     #objs = objs[0:-1]
478
479
480     #retrieves the curves from the objs string
481     for i, l in enumerate(objs):
482         objs[i] = bpy.data.objects[l]
483
484
485
486     #calcs the new vertices coordinates if we change the curves.
487     vxs = loft(objs, steps, spans, intype, tension, bias)
488
489     #apply the new cordinates to the loft object
490     me = resobj.data
491
492     for i in range(0, len(me.vertices)):
493         me.vertices[i].co = vxs[i]
494     me.update()
495     return spans
496
497 #NOTES:
498 #loftdriver function will fail or produce weird results if:
499 #the user changes resobj["spans"] or resobj["steps"]
500 #if we delete any vertex from the loft object
501
502 ### TODO:check if thats the case to remove the drivers
503
504 #creates the drivers expressions for each curve
505 def createloftdriver(objs, res, intype):
506
507     line = ""
508     for obj in objs:
509         line+=obj.name+";"
510     line=line[0:-1]
511     name = res.name
512
513     interp = str(intype)
514
515     for obj in objs:
516         obj["driver"] = 1.0
517
518         obj.driver_add('["driver"]')
519         obj.animation_data.drivers[0].driver.expression = "loftdriver('"+ name +"', '" + line + "', "+interp+")"
520
521
522     ### creating this driver will execute loft all the time without reason,
523     #and if i cant drive the mesh i cannot implement live tension and bias
524
525 #   res['driver'] = 1.0
526 #   if res.animation_data==None:
527 #       res.animation_data_create()
528 #   res.driver_add('["driver"]')
529 #   res.animation_data.drivers[0].driver.expression = "loftdriver('"+ name +"', '" + line + "', "+interp+")"
530
531 #calculates the vertices position of the loft object
532 def loft(objs, steps, spans, interpolation=1, tension=0.0, bias=0.5):
533     verts=[]
534
535     for i in range(0, len(objs)):
536
537         for j in range(0,steps+1):
538             t = 1.0*j/steps
539             verts.append(calct(objs[i], t))
540
541         temp2=[]
542         if i<len(objs)-1:
543             for l in range(1, spans):
544                 tr = 1.0*l/spans
545                 for k in range(0, steps+1):
546                     t=1.0*k/steps
547                     if interpolation:
548                         pos = intc(objs, i, t, tr, interpolation, tension, bias)
549                     else:
550                         pos = intl(objs,i, t, tr)
551
552                     temp2.append(pos)
553             verts.extend(temp2)
554     return verts
555
556
557 #loft operator
558
559 class LoftOperator(bpy.types.Operator):
560     """Tooltip"""
561     bl_idname = "mesh.loft_operator"
562     bl_label = "Loft between bezier curves"
563
564     @classmethod
565     def poll(cls, context):
566         return context.active_object != None
567
568     def execute(self, context):
569         #retrieves the curves in the order they were selected
570         objs = bpy.selection
571
572         spans = context.scene.spans
573         steps = context.scene.steps
574
575         intype = int(context.scene.intype)
576
577         verts = loft(objs, steps, spans, intype)
578
579         nfaces = steps*spans*(len(objs)-1)
580         faces=[]
581         for i in range(0, nfaces):
582             d = int(i/steps)
583             f = [i+d,i+d+1, i+d+steps+2, i+d+steps+1]
584             #inverts normals
585             #f = [i+d,i+d+steps+1, i+d+steps+2, i+d+1]
586             faces.append(f)
587
588
589         me = bpy.data.meshes.new("Loft")
590         me.from_pydata(verts,[], faces)
591         me.update()
592         newobj = bpy.data.objects.new("Loft", me)
593         #newobj.data = me
594         scn = context.scene
595         scn.objects.link(newobj)
596         scn.objects.active = newobj
597         newobj.select = True
598         bpy.ops.object.shade_smooth()
599
600         #the object stores its own steps and spans
601         #this way the driver will know how to deform the mesh
602         newobj["steps"] = steps
603         newobj["spans"] = spans
604
605         if intype==3:
606             newobj['tension'] = context.scene.tension
607             newobj['bias'] = context.scene.bias
608
609
610         if context.scene.dodriver:
611             createloftdriver(objs, newobj, intype)
612
613         return {'FINISHED'}
614
615 class UpdateFix(bpy.types.Operator):
616     """Tooltip"""
617     bl_idname = "mesh.update_fix"
618     bl_label = "Update fix"
619
620     @classmethod
621     def poll(cls, context):
622         return context.active_object != None
623
624     def execute(self, context):
625         #print("------------")
626 #       for it in bpy.app.driver_namespace:
627 #           print(it)
628         bpy.app.driver_namespace['loftdriver'] = loftdriver
629         bpy.app.driver_namespace['birail1driver'] = birail1driver
630         for obj in context.scene.objects:
631             if obj.type=="CURVE" and obj.animation_data!=None and len(obj.animation_data.drivers)>0:
632                 for drv in obj.animation_data.drivers:
633                     if drv.data_path=='["driver"]':
634                         cad = drv.driver.expression
635                         drv.driver.expression = ""
636                         drv.driver.expression = cad
637
638         return {'FINISHED'}
639
640
641 #derives a curve at a given parameter
642 def deriv(curve, t, unit=False):
643
644     a = t + 0.001
645     if t==1: a=t-0.001
646
647     pos = calct(curve, t)
648     der = (pos-calct(curve, a))/(t-a)
649     if unit:
650         der = der/der.magnitude
651     return der
652
653 ### BIRAIL1 BLOCK
654
655
656 #see explanation video about the construction
657 #http://vimeo.com/25455967
658
659 #calculates birail vertices
660
661 ### TODO: when the 3 curves are coplanar it should fail, cause the cross product. check that
662 def birail1(objs, steps, spans, proportional):
663
664     profile=objs[0]
665     ### TODO: identify which path is left or right
666     path1 = objs[1]
667     path2 = objs[2]
668
669     trans = []
670
671     r0 = [calct(path1,0), calct(path2, 0)]
672     r0mag = (r0[1]-r0[0]).magnitude
673
674     for i in range(0, steps):
675         u = i/(steps-1)
676         appr0 = r0[0]+(r0[1]-r0[0])*u
677         trans.append(calct(profile, u)-appr0)
678
679     der10 = deriv(path1, 0)
680     der20 = deriv(path2, 0)
681
682     verts = []
683
684     mult = 1.0
685
686     for i in range(0, spans):
687         v = i/(spans-1)
688         r = [calct(path1, v),calct(path2, v)]
689         rmag = (r[1]-r[0]).magnitude
690
691         der1 = deriv(path1, v)
692         der2 = deriv(path2, v)
693
694         angle1 = der10.angle(der1)
695         angle2 = der20.angle(der2)
696
697         #if angle1!=0.0 and angle2!=0: we can avoid some operations by doing this check but im lazy
698         cr1 = der1.cross(der10)
699         rot1 = Matrix().Rotation(-angle1, 3, cr1)
700
701         cr2 = der2.cross(der20)
702         rot2 = Matrix().Rotation(-angle2, 3, cr2)
703
704         if proportional:
705             mult = rmag/r0mag
706
707         for j in range(0, steps):
708             u = j/(steps-1)
709
710             app = r[0]+(r[1]-r[0])*u
711
712             newtr1 = trans[j].copy()
713             newtr1.rotate(rot1)
714
715             newtr2 = trans[j].copy()
716             newtr2.rotate(rot2)
717
718             r1 = (newtr1-trans[j])*(1-u)
719             r2 = (newtr2-trans[j])*(u)
720
721             res = r1+r2+app+mult*trans[j]
722
723             verts.append(res)
724
725     return verts
726
727
728 #same as loft driver
729 ### TODO check if it is registered
730 def birail1driver(name, objs):
731
732     objs = objs.split(";")
733     #objs = objs[0:-1]
734
735     for i, l in enumerate(objs):
736         objs[i] = bpy.data.objects[l]
737
738     try:
739         resobj = bpy.data.objects[name]
740         spans = resobj["spans"]
741         steps = resobj["steps"]
742         prop = resobj["prop"]
743
744     except:
745         curve = bpy.context.object
746         curve.driver_remove('["driver"]')
747         return False
748
749     vxs = birail1(objs, steps, spans, prop)
750
751     me = resobj.data
752
753     for i in range(0, len(me.vertices)):
754         me.vertices[i].co = vxs[i]
755     me.update()
756     return spans
757
758 def createbirail1driver(objs, res):
759
760     line = ""
761     for obj in objs:
762         line+=obj.name+";"
763     line=line[0:-1]
764     for obj in objs:
765         obj["driver"] = 1.0
766         obj.driver_add('["driver"]')
767         obj.animation_data.drivers[0].driver.expression = "birail1driver('"+ res.name +"', '" + line + "')"
768
769 ### TODO: check polls and if initial variables are ok to perform the birail
770 class Birail1Operator(bpy.types.Operator):
771
772     bl_idname = "mesh.birail1_operator"
773     bl_label = "Birail between 3 bezier curves"
774
775     @classmethod
776     def poll(cls, context):
777         return context.active_object != None
778
779     def execute(self, context):
780
781         objs = bpy.selection
782
783         if len(objs)!=3:
784             self.report({'ERROR'},"Please select 3 curves")
785             return {'FINISHED'}
786
787         scn = context.scene
788         spans = scn.spans
789         steps = scn.steps
790         prop = scn.proportional
791
792         verts = birail1(objs, steps, spans, prop)
793
794         if verts!=[]:
795             faces=[]
796
797             nfaces = (steps-1)*(spans-1)
798
799             for i in range(0, nfaces):
800                 d = int(i/(steps-1))
801                 f = [i+d+1, i+d, i+d+steps, i+d+steps+1 ]
802                 faces.append(f)
803
804             me = bpy.data.meshes.new("Birail")
805             me.from_pydata(verts,[], faces)
806             me.update()
807             newobj = bpy.data.objects.new("Birail", me)
808             newobj.data = me
809
810             scn.objects.link(newobj)
811             scn.objects.active = newobj
812             newobj.select = True
813             bpy.ops.object.shade_smooth()
814             newobj['steps']=steps
815             newobj['spans']=spans
816             newobj['prop']=prop
817
818             if scn.dodriver:
819                 createbirail1driver(objs, newobj)
820
821         return {'FINISHED'}
822
823 #register the drivers
824 bpy.app.driver_namespace['loftdriver'] = loftdriver
825 bpy.app.driver_namespace['birail1driver'] = birail1driver
826
827 ### MERGE SPLINES BLOCK
828
829 #reads spline points
830 #spl spline to read
831 #rev reads the spline forward or backwards
832 def readspline(spl, rev=0):
833     res = []
834
835     if spl.type=="BEZIER":
836         points = spl.bezier_points
837         for p in points:
838             if rev:
839                 h2 = p.handle_left
840                 h1 = p.handle_right
841                 h2type = p.handle_left_type
842                 h1type = p.handle_right_type
843             else:
844                 h1 = p.handle_left
845                 h2 = p.handle_right
846                 h1type = p.handle_left_type
847                 h2type = p.handle_right_type
848
849             co = p.co
850             res.append([h1, co, h2, h1type, h2type])
851     if rev:
852         res.reverse()
853
854     return res
855
856 #returns a new merged spline
857 #cu curve object
858 #pts1 points from the first spline
859 #pts2 points from the second spline
860
861 def merge(cu, pts1, pts2):
862     newspl = cu.data.splines.new(type="BEZIER")
863     for i, p in enumerate(pts1):
864
865         if i>0: newspl.bezier_points.add()
866         newspl.bezier_points[i].handle_left = p[0]
867         newspl.bezier_points[i].co = p[1]
868         newspl.bezier_points[i].handle_right = p[2]
869         newspl.bezier_points[i].handle_left_type = p[3]
870         newspl.bezier_points[i].handle_right_type = p[4]
871
872     newspl.bezier_points[-1].handle_right_type="FREE"
873     newspl.bezier_points[-1].handle_left_type="FREE"
874
875     newspl.bezier_points[-1].handle_right = pts2[0][2]
876
877
878     for j in range(1, len(pts2)):
879
880         newspl.bezier_points.add()
881         newspl.bezier_points[-1].handle_left = pts2[j][0]
882         newspl.bezier_points[-1].co = pts2[j][1]
883         newspl.bezier_points[-1].handle_right = pts2[j][2]
884         newspl.bezier_points[-1].handle_left_type = pts2[j][3]
885         newspl.bezier_points[-1].handle_right_type = pts2[j][4]
886
887     return newspl
888
889 #looks if the splines first and last points are close to another spline
890 ### TODO: Check if the objects selected are valid
891 ### if possible implement nurbs
892
893 class MergeSplinesOperator(bpy.types.Operator):
894
895     bl_idname = "curve.merge_splines"
896     bl_label = "Merges spline points inside a limit"
897
898     @classmethod
899     def poll(cls, context):
900         return context.active_object != None
901
902     def execute(self, context):
903         curves = []
904         limit = context.scene.limit
905         print("merguing")
906         for obj in context.selected_objects:
907             if obj.type=="CURVE":
908                 curves.append(obj)
909
910         for cu in curves:
911             splines = []
912             for spl in cu.data.splines:
913                 splines.append(spl)
914             print(splines)
915             #compares all the splines inside a curve object
916             for spl1 in splines:
917                 for spl2 in splines:
918                     print(spl1, spl2)
919                     if spl1!=spl2 and spl1.type==spl2.type=="BEZIER" and spl1.use_cyclic_u==spl2.use_cyclic_u==False:
920                         print("not cyclic")
921                         if len(spl1.bezier_points)>1  and len(spl2.bezier_points)>1:
922
923                             #edges of the 2 splines
924                             p1i = spl1.bezier_points[0].co
925                             p1f = spl1.bezier_points[-1].co
926                             p2i = spl2.bezier_points[0].co
927                             p2f = spl2.bezier_points[-1].co
928
929                             if dist(p1i, p2i)<limit:
930                                 print("join p1i p2i")
931
932                                 p1 = readspline(spl2, 1)
933                                 p2 = readspline(spl1)
934                                 res=merge(cu, p1, p2)
935                                 cu.data.splines.remove(spl1)
936                                 cu.data.splines.remove(spl2)
937                                 splines.append(res)
938                                 break
939                             elif dist(p1i, p2f)<limit:
940                                 print("join p1i p2f")
941                                 p1 = readspline(spl2)
942                                 p2 = readspline(spl1)
943                                 res = merge(cu, p1, p2)
944                                 cu.data.splines.remove(spl1)
945                                 cu.data.splines.remove(spl2)
946                                 splines.append(res)
947                                 break
948                             elif dist(p1f, p2i)<limit:
949                                 print("join p1f p2i")
950                                 p1 = readspline(spl1)
951                                 p2 = readspline(spl2)
952                                 res = merge(cu, p1, p2)
953                                 cu.data.splines.remove(spl1)
954                                 cu.data.splines.remove(spl2)
955                                 splines.append(res)
956                                 break
957                             elif dist(p1f, p2f)<limit:
958                                 print("unir p1f p2f")
959                                 p1 = readspline(spl1)
960                                 p2 = readspline(spl2, 1)
961                                 res = merge(cu, p1, p2)
962                                 cu.data.splines.remove(spl1)
963                                 cu.data.splines.remove(spl2)
964                                 splines.append(res)
965                                 break
966
967         #splines.remove(spl1)
968         return {'FINISHED'}
969
970 ### NURBS WEIGHTS
971
972 class NurbsWeightsPanel(bpy.types.Panel):
973     bl_label = "Nurbs Weights"
974     bl_space_type = "VIEW_3D"
975     bl_region_type = "TOOLS"
976     #bl_context = "data"
977
978     @classmethod
979     def poll(cls, context):
980         if context.active_object != None and context.active_object.type =="CURVE" and context.active_object.data.splines.active!=None and context.active_object.data.splines.active.type=="NURBS":
981             return True
982         else:
983             return False
984
985
986     def draw(self, context):
987         layout = self.layout
988
989         obj = context.object
990
991         for p in obj.data.splines.active.points:
992             if p.select:
993                 row = layout.row()
994                 row.prop(p,  "weight")
995
996 ### CUT / SUBDIVIDE CURVE
997 #obj curve object
998 #t parameter to perform the cut or split
999 #method True = subdivide, Flase= Split
1000
1001 def cutcurve(obj, t, method=True):
1002
1003     #flocal to global transforms or viceversa
1004
1005
1006     #retrieves the active spline or the first spline if there no one active
1007
1008     spline=None
1009     if obj.data.splines.active==None:
1010         for sp in obj.data.splines:
1011             if sp.type=="BEZIER":
1012                 spline= sp
1013                 break
1014     else:
1015         if obj.data.splines.active.type!="BEZIER":
1016             return False
1017         else:
1018             spline=obj.data.splines.active
1019
1020     if spline==None: return False
1021
1022     points = spline.bezier_points
1023     nsegs = len(points)-1
1024
1025     #transform global t into local t1
1026     d = 1.0/nsegs
1027     seg = int(t/d)
1028     t1 = t/d-int(t/d)
1029     if t>=1.0:
1030         t=1.0
1031         seg-=1
1032         t1 = 1.0
1033
1034     #if t1 is not inside a segment dont perform any action
1035     if t1>0.0 and t1<1.0:
1036         mw = obj.matrix_world
1037         mwi = obj.matrix_world.copy().inverted()
1038
1039         pts = getbezpoints(spline, mw, seg)
1040
1041         #position on the curve to perform the action
1042         pos = calct(obj, t)
1043
1044         #De Casteljau's algorithm to get the handles
1045         #http://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
1046         h1 = pts[0]+(pts[1]-pts[0])*t1
1047         h4 = pts[2]+(pts[3]-pts[2])*t1
1048         r = pts[1]+(pts[2]-pts[1])*t1
1049         h2 = h1+(r-h1)*t1
1050         h3 = r+(h4-r)*t1
1051
1052
1053         if method:
1054             #SUBDIVIDE
1055             splp = []
1056             type = "ALIGNED"
1057             for i, p in enumerate(points):
1058                 ph1 = p.handle_left*mw
1059                 pco = p.co*mw
1060                 ph2 = p.handle_right*mw
1061                 ph1type = p.handle_left_type
1062                 ph2type = p.handle_right_type
1063                 splp.append([ph1, pco, ph2, ph1type, ph2type])
1064                 p.handle_left_type = type
1065                 p.handle_right_type = type
1066
1067                 if i==seg:
1068                     splp[-1][2]=h1
1069                     splp.append([h2, pos, h3, type, type])
1070
1071                 if i==seg+1:
1072                     splp[-1][0]=h4
1073                     splp[-1][3]=type
1074                     splp[-1][4]=type
1075                 #if i dont set all the handles to "FREE"
1076                 #it returns weirds result
1077                 ### TODO: find out how to preserve handle's types
1078
1079             points.add()
1080             for i, p in enumerate(points):
1081                 p.handle_left_type = "FREE"
1082                 p.handle_right_type ="FREE"
1083                 p.handle_left = splp[i][0]*mwi
1084                 p.co = splp[i][1]*mwi
1085                 p.handle_right=splp[i][2]*mwi
1086                 p.handle_left_type = splp[i][3]
1087                 p.handle_right_type =splp[i][4]
1088         else:
1089             #SPLIT CURVE
1090             spl1 = []
1091             spl2 = []
1092             k=0 #changes to 1 when the first spline is processed
1093             type = "ALIGNED"
1094             for i, p in enumerate(points):
1095                 ph1 = p.handle_left*mw
1096                 pco = p.co*mw
1097                 ph2 = p.handle_right*mw
1098                 ph1type = p.handle_left_type
1099                 ph2type = p.handle_right_type
1100                 if k==0:
1101                     spl1.append([ph1, pco, ph2, ph1type, ph2type])
1102                 else:
1103                     spl2.append([ph1, pco, ph2, ph1type, ph2type])
1104
1105                 if i==seg:
1106                     spl1[-1][2]=h1
1107                     spl1.append([h2, pos, h3, type, type])
1108                     spl2.append([h2, pos, h3, type, type])
1109                     k=1
1110
1111                 if i==seg+1:
1112                     spl2[-1][0]=h4
1113                     spl2[-1][3]=type
1114                     spl2[-1][4]=type
1115
1116             sp1 = obj.data.splines.new(type="BEZIER")
1117             for i, p in enumerate(spl1):
1118                 if i>0: sp1.bezier_points.add()
1119                 sp1.bezier_points[i].handle_left_type = "FREE"
1120                 sp1.bezier_points[i].handle_right_type ="FREE"
1121                 sp1.bezier_points[i].handle_left = spl1[i][0]*mwi
1122                 sp1.bezier_points[i].co = spl1[i][1]*mwi
1123                 sp1.bezier_points[i].handle_right=spl1[i][2]*mwi
1124                 #i tried to preserve the handles here but
1125                 #didnt work well
1126
1127                 sp1.bezier_points[i].handle_left_type = spl1[i][3]
1128                 sp1.bezier_points[i].handle_right_type =spl1[i][4]
1129
1130             sp2 = obj.data.splines.new(type="BEZIER")
1131             for i, p in enumerate(spl2):
1132                 if i>0: sp2.bezier_points.add()
1133                 sp2.bezier_points[i].handle_left_type = "FREE"
1134                 sp2.bezier_points[i].handle_right_type = "FREE"
1135                 sp2.bezier_points[i].handle_left = spl2[i][0]*mwi
1136                 sp2.bezier_points[i].co = spl2[i][1]*mwi
1137                 sp2.bezier_points[i].handle_right=spl2[i][2]*mwi
1138                 sp2.bezier_points[i].handle_left_type = spl2[i][3]
1139                 sp2.bezier_points[i].handle_right_type =spl2[i][4]
1140
1141             obj.data.splines.remove(spline)
1142
1143 class CutCurveOperator(bpy.types.Operator):
1144     """Subdivide / Split a bezier curve"""
1145     bl_idname = "curve.cut_operator"
1146     bl_label = "Cut curve operator"
1147
1148     #cut or split
1149     method = bpy.props.BoolProperty(default=False)
1150     t = 0.0
1151
1152     @classmethod
1153     def poll(self, context):
1154         if context.active_object!=None:
1155             return context.active_object.type=="CURVE"
1156         else:
1157             return False
1158
1159
1160     def modal(self, context, event):
1161
1162         if event.type == 'MOUSEMOVE':
1163             #full screen width
1164             #not tested for multiple monitors
1165             fullw = context.window_manager.windows[0].screen.areas[0].regions[0].width
1166
1167             self.t = event.mouse_x/fullw
1168
1169             #limit t to [0,...,1]
1170             if self.t<0:
1171                 self.t=0.0
1172             elif self.t>1.0:
1173                 self.t=1.0
1174
1175             obj = context.object
1176             pos = calct(obj, self.t)
1177
1178             #if calct() detects a non bezier spline returns false
1179             if pos==False:
1180                 return {'CANCELLED'}
1181             cursor(pos)
1182
1183         elif event.type == 'LEFTMOUSE':
1184             #print(self.method, self.t)
1185             cutcurve(context.object, self.t, self.method)
1186             return {'FINISHED'}
1187
1188         elif event.type in {'RIGHTMOUSE', 'ESC'}:
1189             #print("Cancelled")
1190
1191             return {'CANCELLED'}
1192
1193         return {'RUNNING_MODAL'}
1194
1195     def invoke(self, context, event):
1196
1197         if context.object:
1198             context.window_manager.modal_handler_add(self)
1199             return {'RUNNING_MODAL'}
1200         else:
1201             self.report({'WARNING'}, "No active object, could not finish")
1202             return {'CANCELLED'}
1203
1204 ### CURVE SNAP BLOCK
1205
1206 class AllowCurveSnap(bpy.types.Operator):
1207     bl_idname = "curve.allow_curve_snap"
1208     bl_label = "Allow Curve Snap"
1209
1210     add = bpy.props.BoolProperty()
1211
1212     @classmethod
1213     def poll(cls, context):
1214         return context.active_object!=None
1215
1216     def execute(self, context):
1217         add = self.add
1218
1219         scn = context.scene
1220
1221         if add==False:
1222             for helper in context.scene.objects:
1223                 for key  in helper.keys():
1224                     print(key)
1225                     if key=="is_snap_helper" and helper[key]==1:
1226                         scn.objects.unlink(helper)
1227         else:
1228             #objs = context.selected_objects
1229             objs = context.scene.objects
1230             for obj in objs:
1231                 if obj.type=="CURVE":
1232
1233                     res = obj.data.resolution_u
1234
1235                     obj.data.resolution_u = 100
1236
1237                     me = obj.to_mesh(scene=scn, apply_modifiers=True,settings = "PREVIEW" )
1238                     obj.data.resolution_u = res
1239                     newobj = bpy.data.objects.new(obj.name+"_snap", me)
1240                     scn.objects.link(newobj)
1241                     newobj.layers = obj.layers
1242                     newobj.matrix_world = obj.matrix_world
1243                     newobj["is_snap_helper"]=True
1244                     newobj.hide_render=True
1245                     newobj.hide_select = True
1246                     cons = newobj.constraints.new(type="COPY_TRANSFORMS")
1247                     cons.target =obj
1248
1249         return {'FINISHED'}
1250
1251 def menu_func(self, context):
1252     self.layout.operator("curve.allow_curve_snap").add=True
1253     self.layout.operator("curve.allow_curve_snap", text = "Delete Snap Helpers").add=False
1254
1255 ### PANEL
1256 class CurvePanel(bpy.types.Panel):
1257     bl_label = "Curve Tools"
1258     bl_space_type = "VIEW_3D"
1259     bl_region_type = "TOOLS"
1260     #bl_options = {'REGISTER', 'UNDO'}
1261     #bl_context = "data"
1262     bl_options = {'DEFAULT_CLOSED'}
1263     steps = IntProperty(min=2, default = 12)
1264
1265     @classmethod
1266     def poll(cls, context):
1267         return (context.active_object != None) and (context.active_object.type=="CURVE")
1268     def draw(self, context):
1269         layout = self.layout
1270
1271         obj = context.object
1272         scn = context.scene
1273
1274         align = True
1275         row = layout.row(align=align)
1276
1277         row.prop(context.scene, "intype", text = "")
1278         row.prop(context.scene, "dodriver", text = "Driven")
1279         if scn.intype=='3': #Hermite interp
1280             row = layout.row(align=align)
1281             row.prop(scn, "tension")
1282             row.prop(scn, "bias")
1283
1284         row = layout.row(align=align)
1285         row.prop(context.scene, "steps")
1286         row.prop(context.scene, "spans")
1287         row = layout.row(align=align)
1288         row.operator("mesh.loft_operator", text = "Loft")
1289         row.operator("mesh.update_fix", text = "Update Fix")
1290         row = layout.row(align=align)
1291         row.operator("mesh.birail1_operator", text = "Birail 1")
1292         row.prop(context.scene, "proportional", text = "Proportional")
1293         row = layout.row(align=align)
1294         row.operator("curve.arc_length_operator", text = "Calc Length")
1295         row.prop(context.scene, "clen", text = "")
1296         row = layout.row(align=align)
1297         row.operator("curve.merge_splines", text = "Merge")
1298         row.prop(context.scene,"limit",  text = "Limit")
1299         row = layout.row(align=align)
1300         row.operator("curve.cut_operator", text="Subdivide").method=True
1301         row.operator("curve.cut_operator", text="Split").method=False
1302
1303 #       col1 = row.column()
1304 #       col1.prop(context.scene, "intype", text = "")
1305 #       col1.prop(context.scene, "dodriver", text = "Driven")
1306 #       row = layout.row(align=align)
1307 #       col2 = row.column(align=align)
1308 #       col2.prop(context.scene, "steps")
1309 #       col2.prop(context.scene, "spans")
1310 #       row = layout.row(align=align)
1311 #       row.operator("mesh.loft_operator", text = "Loft")
1312 #       row.operator("mesh.update_fix", text = "Update Fix")
1313 #       row = layout.row(align=align)
1314 #       row.operator("mesh.birail1_operator", text = "Birail 1")
1315 #       row.prop(context.scene, "proportional", text = "Proportional")
1316 #       row = layout.row(align=align)
1317 #       row.operator("curve.arc_length_operator", text = "Calc Length")
1318 #       row.prop(context.scene, "clen", text = "")
1319 #       row = layout.row(align=align)
1320 #       row.operator("curve.merge_splines", text = "Merge")
1321 #       row.prop(context.scene,"limit",  text = "Limit")
1322 #       row = layout.row(align=align)
1323 #       row.operator("curve.cut_operator", text="Subdivide").method=True
1324 #       row.operator("curve.cut_operator", text="Split").method=False
1325
1326 classes = [AllowCurveSnap, Selection, LoftOperator, Birail1Operator,
1327             ArcLengthOperator, UpdateFix, MergeSplinesOperator, CutCurveOperator, NurbsWeightsPanel, CurvePanel]
1328
1329 bpy.app.driver_namespace['loftdriver'] = loftdriver
1330 bpy.app.driver_namespace['birail1driver'] = birail1driver
1331
1332 def register():
1333
1334     domenu=1
1335     for op in dir(bpy.types):
1336         if op=="CURVE_OT_allow_curve_snap":
1337             domenu=0
1338             break
1339     if domenu:
1340         bpy.types.VIEW3D_MT_object_specials.append(menu_func)
1341
1342     for c in classes:
1343         bpy.utils.register_class(c)
1344
1345 def unregister():
1346     for c in classes:
1347         bpy.utils.unregister_class(c)
1348
1349     bpy.types.VIEW3D_MT_object_specials.remove(menu_func)
1350
1351
1352 if __name__ == "__main__":
1353     register()