Use URL icon, add Tip: prefix, increase lower margin
[blender-addons-contrib.git] / curve_tools / Curves.py
1 from . import Math
2
3 import bpy
4
5
6 class BezierPoint:
7     @staticmethod
8     def FromBlenderBezierPoint(blenderBezierPoint):
9         return BezierPoint(blenderBezierPoint.handle_left, blenderBezierPoint.co, blenderBezierPoint.handle_right)
10         
11         
12     def __init__(self, handle_left, co, handle_right):
13         self.handle_left = handle_left
14         self.co = co
15         self.handle_right = handle_right
16     
17     
18     def Copy(self):
19         return BezierPoint(self.handle_left.copy(), self.co.copy(), self.handle_right.copy())
20         
21     def Reversed(self):
22         return BezierPoint(self.handle_right, self.co, self.handle_left)
23         
24     def Reverse(self):
25         tmp = self.handle_left
26         self.handle_left = self.handle_right
27         self.handle_right = tmp
28
29
30 class BezierSegment:
31     @staticmethod
32     def FromBlenderBezierPoints(blenderBezierPoint1, blenderBezierPoint2):
33         bp1 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint1)
34         bp2 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint2)
35
36         return BezierSegment(bp1, bp2)
37         
38         
39     def Copy(self):
40         return BezierSegment(self.bezierPoint1.Copy(), self.bezierPoint2.Copy())
41         
42     def Reversed(self):
43         return BezierSegment(self.bezierPoint2.Reversed(), self.bezierPoint1.Reversed())
44         
45     def Reverse(self):
46         # make a copy, otherwise neighboring segment may be affected
47         tmp = self.bezierPoint1.Copy()
48         self.bezierPoint1 = self.bezierPoint2.Copy()
49         self.bezierPoint2 = tmp
50         self.bezierPoint1.Reverse()
51         self.bezierPoint2.Reverse()
52     
53     
54     def __init__(self, bezierPoint1, bezierPoint2):
55         # bpy.types.BezierSplinePoint
56         # ## NOTE/TIP: copy() helps with repeated (intersection) action -- ??
57         self.bezierPoint1 = bezierPoint1.Copy()
58         self.bezierPoint2 = bezierPoint2.Copy()
59         
60         self.ctrlPnt0 = self.bezierPoint1.co
61         self.ctrlPnt1 = self.bezierPoint1.handle_right
62         self.ctrlPnt2 = self.bezierPoint2.handle_left
63         self.ctrlPnt3 = self.bezierPoint2.co
64         
65         self.coeff0 = self.ctrlPnt0
66         self.coeff1 = self.ctrlPnt0 * (-3.0) + self.ctrlPnt1 * (+3.0)
67         self.coeff2 = self.ctrlPnt0 * (+3.0) + self.ctrlPnt1 * (-6.0) + self.ctrlPnt2 * (+3.0)
68         self.coeff3 = self.ctrlPnt0 * (-1.0) + self.ctrlPnt1 * (+3.0) + self.ctrlPnt2 * (-3.0) + self.ctrlPnt3
69         
70         
71     def CalcPoint(self, parameter = 0.5):
72         parameter2 = parameter * parameter
73         parameter3 = parameter * parameter2
74         
75         rvPoint = self.coeff0 + self.coeff1 * parameter + self.coeff2 * parameter2 + self.coeff3 * parameter3
76         
77         return rvPoint
78         
79         
80     def CalcDerivative(self, parameter = 0.5):
81         parameter2 = parameter * parameter
82         
83         rvPoint = self.coeff1 + self.coeff2 * parameter * 2.0 + self.coeff3 * parameter2 * 3.0
84         
85         return rvPoint
86         
87
88     def CalcLength(self, nrSamples = 2):
89         nrSamplesFloat = float(nrSamples)
90         rvLength = 0.0
91         for iSample in range(nrSamples):
92             par1 = float(iSample) / nrSamplesFloat
93             par2 = float(iSample + 1) / nrSamplesFloat
94             
95             point1 = self.CalcPoint(parameter = par1)
96             point2 = self.CalcPoint(parameter = par2)
97             diff12 = point1 - point2
98             
99             rvLength += diff12.magnitude
100         
101         return rvLength
102         
103     
104     #http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
105     def CalcSplitPoint(self, parameter = 0.5):
106         par1min = 1.0 - parameter
107         
108         bez00 = self.ctrlPnt0
109         bez01 = self.ctrlPnt1
110         bez02 = self.ctrlPnt2
111         bez03 = self.ctrlPnt3
112         
113         bez10 = bez00 * par1min + bez01 * parameter
114         bez11 = bez01 * par1min + bez02 * parameter
115         bez12 = bez02 * par1min + bez03 * parameter
116         
117         bez20 = bez10 * par1min + bez11 * parameter
118         bez21 = bez11 * par1min + bez12 * parameter
119         
120         bez30 = bez20 * par1min + bez21 * parameter
121         
122         bezPoint1 = BezierPoint(self.bezierPoint1.handle_left, bez00, bez10)
123         bezPointNew = BezierPoint(bez20, bez30, bez21)
124         bezPoint2 = BezierPoint(bez12, bez03, self.bezierPoint2.handle_right)
125         
126         return [bezPoint1, bezPointNew, bezPoint2]
127             
128
129 class BezierSpline:
130     @staticmethod
131     def FromSegments(listSegments):
132         rvSpline = BezierSpline(None)
133         
134         rvSpline.segments = listSegments
135         
136         return rvSpline
137     
138     
139     def __init__(self, blenderBezierSpline):
140         if not blenderBezierSpline is None:
141             if blenderBezierSpline.type != 'BEZIER': 
142                 print("## ERROR:", "blenderBezierSpline.type != 'BEZIER'")
143                 raise Exception("blenderBezierSpline.type != 'BEZIER'")
144             if len(blenderBezierSpline.bezier_points) < 1:
145                 if not blenderBezierSpline.use_cyclic_u:
146                     print("## ERROR:", "len(blenderBezierSpline.bezier_points) < 1")
147                     raise Exception("len(blenderBezierSpline.bezier_points) < 1")
148         
149         self.bezierSpline = blenderBezierSpline
150         
151         self.resolution = 12
152         self.isCyclic = False
153         if not self.bezierSpline is None:
154             self.resolution = self.bezierSpline.resolution_u
155             self.isCyclic = self.bezierSpline.use_cyclic_u
156             
157         self.segments = self.SetupSegments()
158         
159         
160     def __getattr__(self, attrName):
161         if attrName == "nrSegments":
162             return len(self.segments)
163         
164         if attrName == "bezierPoints":
165             rvList = []
166             
167             for seg in self.segments: rvList.append(seg.bezierPoint1)
168             if not self.isCyclic: rvList.append(self.segments[-1].bezierPoint2)
169             
170             return rvList
171         
172         if attrName == "resolutionPerSegment":
173             try: rvResPS = int(self.resolution / self.nrSegments)
174             except: rvResPS = 2
175             if rvResPS < 2: rvResPS = 2
176             
177             return rvResPS
178         
179         if attrName == "length":
180             return self.CalcLength()
181         
182         return None
183         
184         
185     def SetupSegments(self):
186         rvSegments = []
187         if self.bezierSpline is None: return rvSegments
188         
189         nrBezierPoints = len(self.bezierSpline.bezier_points)
190         for iBezierPoint in range(nrBezierPoints - 1):
191             bezierPoint1 = self.bezierSpline.bezier_points[iBezierPoint]
192             bezierPoint2 = self.bezierSpline.bezier_points[iBezierPoint + 1]
193             rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
194         if self.isCyclic:
195             bezierPoint1 = self.bezierSpline.bezier_points[-1]
196             bezierPoint2 = self.bezierSpline.bezier_points[0]
197             rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
198         
199         return rvSegments
200         
201         
202     def UpdateSegments(self, newSegments):
203         prevNrSegments = len(self.segments)
204         diffNrSegments = len(newSegments) - prevNrSegments
205         if diffNrSegments > 0:
206             newBezierPoints = []
207             for segment in newSegments: newBezierPoints.append(segment.bezierPoint1)
208             if not self.isCyclic: newBezierPoints.append(newSegments[-1].bezierPoint2)
209         
210             self.bezierSpline.bezier_points.add(diffNrSegments)
211             
212             for i, bezPoint in enumerate(newBezierPoints):
213                 blBezPoint = self.bezierSpline.bezier_points[i]
214                 
215                 blBezPoint.tilt = 0
216                 blBezPoint.radius = 1.0
217                 
218                 blBezPoint.handle_left_type = 'FREE'
219                 blBezPoint.handle_left = bezPoint.handle_left
220                 blBezPoint.co = bezPoint.co
221                 blBezPoint.handle_right_type = 'FREE'
222                 blBezPoint.handle_right = bezPoint.handle_right    
223
224             self.segments = newSegments
225         else:
226             print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
227             
228     
229     def Reversed(self):
230         revSegments = []
231         
232         for iSeg in reversed(range(self.nrSegments)): revSegments.append(self.segments[iSeg].Reversed())
233         
234         rvSpline = BezierSpline.FromSegments(revSegments)
235         rvSpline.resolution = self.resolution
236         rvSpline.isCyclic = self.isCyclic
237         
238         return rvSpline
239             
240     
241     def Reverse(self):
242         revSegments = []
243         
244         for iSeg in reversed(range(self.nrSegments)): 
245             self.segments[iSeg].Reverse()
246             revSegments.append(self.segments[iSeg])
247         
248         self.segments = revSegments
249         
250         
251     def CalcDivideResolution(self, segment, parameter):
252         if not segment in self.segments:
253             print("### WARNING: InsertPoint(): not segment in self.segments")
254             return None
255             
256         iSeg = self.segments.index(segment)
257         dPar = 1.0 / self.nrSegments
258         splinePar = dPar * (parameter + float(iSeg))
259         
260         res1 = int(splinePar * self.resolution)
261         if res1 < 2:
262             print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1, "-- setting it to 2")
263             res1 = 2
264             
265         res2 = int((1.0 - splinePar) * self.resolution)
266         if res2 < 2:
267             print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2, "-- setting it to 2")
268             res2 = 2
269         
270         return [res1, res2]
271         # return [self.resolution, self.resolution]
272         
273         
274     def CalcPoint(self, parameter):
275         nrSegs = self.nrSegments
276         
277         segmentIndex = int(nrSegs * parameter)
278         if segmentIndex < 0: segmentIndex = 0
279         if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
280         
281         segmentParameter = nrSegs * parameter - segmentIndex
282         if segmentParameter < 0.0: segmentParameter = 0.0
283         if segmentParameter > 1.0: segmentParameter = 1.0
284         
285         return self.segments[segmentIndex].CalcPoint(parameter = segmentParameter)
286         
287         
288     def CalcDerivative(self, parameter):
289         nrSegs = self.nrSegments
290         
291         segmentIndex = int(nrSegs * parameter)
292         if segmentIndex < 0: segmentIndex = 0
293         if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
294         
295         segmentParameter = nrSegs * parameter - segmentIndex
296         if segmentParameter < 0.0: segmentParameter = 0.0
297         if segmentParameter > 1.0: segmentParameter = 1.0
298         
299         return self.segments[segmentIndex].CalcDerivative(parameter = segmentParameter)
300         
301         
302     def InsertPoint(self, segment, parameter):
303         if not segment in self.segments:
304             print("### WARNING: InsertPoint(): not segment in self.segments")
305             return
306         iSeg = self.segments.index(segment)
307         nrSegments = len(self.segments)
308             
309         splitPoints = segment.CalcSplitPoint(parameter = parameter)
310         bezPoint1 = splitPoints[0]
311         bezPointNew = splitPoints[1]
312         bezPoint2 = splitPoints[2]
313         
314         segment.bezierPoint1.handle_right = bezPoint1.handle_right
315         segment.bezierPoint2 = bezPointNew
316         
317         if iSeg < (nrSegments - 1): 
318             nextSeg = self.segments[iSeg + 1]
319             nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
320         else:
321             if self.isCyclic:
322                 nextSeg = self.segments[0]
323                 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
324             
325         
326         newSeg = BezierSegment(bezPointNew, bezPoint2)
327         self.segments.insert(iSeg + 1, newSeg)
328         
329         
330     def Split(self, segment, parameter):
331         if not segment in self.segments:
332             print("### WARNING: InsertPoint(): not segment in self.segments")
333             return None
334         iSeg = self.segments.index(segment)
335         nrSegments = len(self.segments)
336             
337         splitPoints = segment.CalcSplitPoint(parameter = parameter)
338         bezPoint1 = splitPoints[0]
339         bezPointNew = splitPoints[1]
340         bezPoint2 = splitPoints[2]
341         
342                 
343         newSpline1Segments = []
344         for iSeg1 in range(iSeg): newSpline1Segments.append(self.segments[iSeg1])
345         if len(newSpline1Segments) > 0: newSpline1Segments[-1].bezierPoint2.handle_right = bezPoint1.handle_right
346         newSpline1Segments.append(BezierSegment(bezPoint1, bezPointNew))
347         
348         newSpline2Segments = []
349         newSpline2Segments.append(BezierSegment(bezPointNew, bezPoint2))
350         for iSeg2 in range(iSeg + 1, nrSegments): newSpline2Segments.append(self.segments[iSeg2])
351         if len(newSpline2Segments) > 1: newSpline2Segments[1].bezierPoint1.handle_left = newSpline2Segments[0].bezierPoint2.handle_left
352         
353         
354         newSpline1 = BezierSpline.FromSegments(newSpline1Segments)
355         newSpline2 = BezierSpline.FromSegments(newSpline2Segments)
356         
357         return [newSpline1, newSpline2]
358
359
360     def Join(self, spline2, mode = 'At midpoint'):
361         if mode == 'At midpoint':
362             self.JoinAtMidpoint(spline2)
363             return
364             
365         if mode == 'Insert segment':
366             self.JoinInsertSegment(spline2)
367             return
368             
369         print("### ERROR: Join(): unknown mode:", mode)        
370
371
372     def JoinAtMidpoint(self, spline2):
373         bezPoint1 = self.segments[-1].bezierPoint2
374         bezPoint2 = spline2.segments[0].bezierPoint1
375     
376         mpHandleLeft = bezPoint1.handle_left.copy()
377         mpCo = (bezPoint1.co + bezPoint2.co) * 0.5
378         mpHandleRight = bezPoint2.handle_right.copy()
379         mpBezPoint = BezierPoint(mpHandleLeft, mpCo, mpHandleRight)
380         
381         self.segments[-1].bezierPoint2 = mpBezPoint
382         spline2.segments[0].bezierPoint1 = mpBezPoint
383         for seg2 in spline2.segments: self.segments.append(seg2)
384         
385         self.resolution += spline2.resolution
386         self.isCyclic = False    # is this ok?
387
388
389     def JoinInsertSegment(self, spline2):
390         self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1))
391         for seg2 in spline2.segments: self.segments.append(seg2)
392         
393         self.resolution += spline2.resolution    # extra segment will usually be short -- impact on resolution negligable
394         
395         self.isCyclic = False    # is this ok?
396         
397         
398     def RefreshInScene(self):
399         bezierPoints = self.bezierPoints
400         
401         currNrBezierPoints = len(self.bezierSpline.bezier_points)
402         diffNrBezierPoints = len(bezierPoints) - currNrBezierPoints
403         if diffNrBezierPoints > 0: self.bezierSpline.bezier_points.add(diffNrBezierPoints)
404         
405         for i, bezPoint in enumerate(bezierPoints):
406             blBezPoint = self.bezierSpline.bezier_points[i]
407             
408             blBezPoint.tilt = 0
409             blBezPoint.radius = 1.0
410             
411             blBezPoint.handle_left_type = 'FREE'
412             blBezPoint.handle_left = bezPoint.handle_left
413             blBezPoint.co = bezPoint.co
414             blBezPoint.handle_right_type = 'FREE'
415             blBezPoint.handle_right = bezPoint.handle_right
416
417         self.bezierSpline.use_cyclic_u = self.isCyclic
418         self.bezierSpline.resolution_u = self.resolution
419         
420     
421     def CalcLength(self):
422         try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
423         except: nrSamplesPerSegment = 2
424         if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
425         
426         rvLength = 0.0
427         for segment in self.segments:
428             rvLength += segment.CalcLength(nrSamples = nrSamplesPerSegment)
429         
430         return rvLength
431         
432     
433     def GetLengthIsSmallerThan(self, threshold):
434         try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
435         except: nrSamplesPerSegment = 2
436         if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
437         
438         length = 0.0
439         for segment in self.segments:
440             length += segment.CalcLength(nrSamples = nrSamplesPerSegment)
441             if not length < threshold: return False
442         
443         return True
444         
445         
446 class Curve:
447     def __init__(self, blenderCurve):
448         self.curve = blenderCurve
449         self.curveData = blenderCurve.data
450         
451         self.splines = self.SetupSplines()
452         
453         
454     def __getattr__(self, attrName):
455         if attrName == "nrSplines":
456             return len(self.splines)
457         
458         if attrName == "length":
459             return self.CalcLength()
460         
461         if attrName == "worldMatrix":
462             return self.curve.matrix_world
463         
464         if attrName == "location":
465             return self.curve.location
466         
467         return None
468         
469         
470     def SetupSplines(self):
471         rvSplines = []
472         for spline in self.curveData.splines:
473             if spline.type != 'BEZIER':
474                 print("## WARNING: only bezier splines are supported, atm; other types are ignored")
475                 continue
476             
477             try: newSpline = BezierSpline(spline)
478             except: 
479                 print("## EXCEPTION: newSpline = BezierSpline(spline)")
480                 continue
481             
482             rvSplines.append(newSpline)
483         
484         return rvSplines
485         
486         
487     def RebuildInScene(self):
488         self.curveData.splines.clear()
489         
490         for spline in self.splines:
491             blSpline = self.curveData.splines.new('BEZIER')
492             blSpline.use_cyclic_u = spline.isCyclic
493             blSpline.resolution_u = spline.resolution
494             
495             bezierPoints = []
496             for segment in spline.segments: bezierPoints.append(segment.bezierPoint1)
497             if not spline.isCyclic: bezierPoints.append(spline.segments[-1].bezierPoint2)
498             #else: print("????", "spline.isCyclic")
499             
500             nrBezierPoints = len(bezierPoints)
501             blSpline.bezier_points.add(nrBezierPoints - 1)
502                         
503             for i, blBezPoint in enumerate(blSpline.bezier_points):
504                 bezPoint = bezierPoints[i]
505                 
506                 blBezPoint.tilt = 0
507                 blBezPoint.radius = 1.0
508                 
509                 blBezPoint.handle_left_type = 'FREE'
510                 blBezPoint.handle_left = bezPoint.handle_left
511                 blBezPoint.co = bezPoint.co
512                 blBezPoint.handle_right_type = 'FREE'
513                 blBezPoint.handle_right = bezPoint.handle_right
514         
515     
516     def CalcLength(self):
517         rvLength = 0.0
518         for spline in self.splines:
519             rvLength += spline.length
520         
521         return rvLength
522         
523         
524     def RemoveShortSplines(self, threshold):
525         splinesToRemove = []
526         
527         for spline in self.splines:
528             if spline.GetLengthIsSmallerThan(threshold): splinesToRemove.append(spline)
529             
530         for spline in splinesToRemove: self.splines.remove(spline)
531         
532         return len(splinesToRemove)
533         
534         
535     def JoinNeighbouringSplines(self, startEnd, threshold, mode):
536         nrJoins = 0
537         
538         while True:
539             firstPair = self.JoinGetFirstPair(startEnd, threshold)
540             if firstPair is None: break
541             
542             firstPair[0].Join(firstPair[1], mode)
543             self.splines.remove(firstPair[1])
544             
545             nrJoins += 1
546             
547         return nrJoins
548             
549     
550     def JoinGetFirstPair(self, startEnd, threshold):
551         nrSplines = len(self.splines)
552         
553         if startEnd:
554             for iCurrentSpline in range(nrSplines):
555                 currentSpline = self.splines[iCurrentSpline]
556                 
557                 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
558                     nextSpline = self.splines[iNextSpline]
559                     
560                     currEndPoint = currentSpline.segments[-1].bezierPoint2.co
561                     nextStartPoint = nextSpline.segments[0].bezierPoint1.co
562                     if Math.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
563                     
564                     nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
565                     currStartPoint = currentSpline.segments[0].bezierPoint1.co
566                     if Math.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
567                     
568             return None
569         else:
570             for iCurrentSpline in range(nrSplines):
571                 currentSpline = self.splines[iCurrentSpline]
572                 
573                 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
574                     nextSpline = self.splines[iNextSpline]
575                     
576                     currEndPoint = currentSpline.segments[-1].bezierPoint2.co
577                     nextStartPoint = nextSpline.segments[0].bezierPoint1.co
578                     if Math.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
579                     
580                     nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
581                     currStartPoint = currentSpline.segments[0].bezierPoint1.co
582                     if Math.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
583                     
584                     if Math.IsSamePoint(currEndPoint, nextEndPoint, threshold):
585                         nextSpline.Reverse()
586                         #print("## ", "nextSpline.Reverse()")
587                         return [currentSpline, nextSpline]
588                     
589                     if Math.IsSamePoint(currStartPoint, nextStartPoint, threshold):
590                         currentSpline.Reverse()
591                         #print("## ", "currentSpline.Reverse()")
592                         return [currentSpline, nextSpline]
593                         
594             return None