1from . import mathematics
2
3import bpy
4
5
6class 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
30class 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
129class 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
446class 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 mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
563
564                    nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
565                    currStartPoint = currentSpline.segments[0].bezierPoint1.co
566                    if mathematics.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 mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
579
580                    nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
581                    currStartPoint = currentSpline.segments[0].bezierPoint1.co
582                    if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
583
584                    if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold):
585                        nextSpline.Reverse()
586                        #print("## ", "nextSpline.Reverse()")
587                        return [currentSpline, nextSpline]
588
589                    if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold):
590                        currentSpline.Reverse()
591                        #print("## ", "currentSpline.Reverse()")
592                        return [currentSpline, nextSpline]
593
594            return None
595