Skip to content
Snippets Groups Projects
curves.py 20.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    
    import bpy
    
    
    class BezierPoint:
        @staticmethod
        def FromBlenderBezierPoint(blenderBezierPoint):
            return BezierPoint(blenderBezierPoint.handle_left, blenderBezierPoint.co, blenderBezierPoint.handle_right)
    
    
        def __init__(self, handle_left, co, handle_right):
            self.handle_left = handle_left
            self.co = co
            self.handle_right = handle_right
    
    
        def Copy(self):
            return BezierPoint(self.handle_left.copy(), self.co.copy(), self.handle_right.copy())
    
        def Reversed(self):
            return BezierPoint(self.handle_right, self.co, self.handle_left)
    
        def Reverse(self):
            tmp = self.handle_left
            self.handle_left = self.handle_right
            self.handle_right = tmp
    
    
    class BezierSegment:
        @staticmethod
        def FromBlenderBezierPoints(blenderBezierPoint1, blenderBezierPoint2):
            bp1 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint1)
            bp2 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint2)
    
            return BezierSegment(bp1, bp2)
    
    
        def Copy(self):
            return BezierSegment(self.bezierPoint1.Copy(), self.bezierPoint2.Copy())
    
        def Reversed(self):
            return BezierSegment(self.bezierPoint2.Reversed(), self.bezierPoint1.Reversed())
    
        def Reverse(self):
            # make a copy, otherwise neighboring segment may be affected
            tmp = self.bezierPoint1.Copy()
            self.bezierPoint1 = self.bezierPoint2.Copy()
            self.bezierPoint2 = tmp
            self.bezierPoint1.Reverse()
            self.bezierPoint2.Reverse()
    
    
        def __init__(self, bezierPoint1, bezierPoint2):
            # bpy.types.BezierSplinePoint
            # ## NOTE/TIP: copy() helps with repeated (intersection) action -- ??
            self.bezierPoint1 = bezierPoint1.Copy()
            self.bezierPoint2 = bezierPoint2.Copy()
    
            self.ctrlPnt0 = self.bezierPoint1.co
            self.ctrlPnt1 = self.bezierPoint1.handle_right
            self.ctrlPnt2 = self.bezierPoint2.handle_left
            self.ctrlPnt3 = self.bezierPoint2.co
    
            self.coeff0 = self.ctrlPnt0
            self.coeff1 = self.ctrlPnt0 * (-3.0) + self.ctrlPnt1 * (+3.0)
            self.coeff2 = self.ctrlPnt0 * (+3.0) + self.ctrlPnt1 * (-6.0) + self.ctrlPnt2 * (+3.0)
            self.coeff3 = self.ctrlPnt0 * (-1.0) + self.ctrlPnt1 * (+3.0) + self.ctrlPnt2 * (-3.0) + self.ctrlPnt3
    
    
        def CalcPoint(self, parameter = 0.5):
            parameter2 = parameter * parameter
            parameter3 = parameter * parameter2
    
            rvPoint = self.coeff0 + self.coeff1 * parameter + self.coeff2 * parameter2 + self.coeff3 * parameter3
    
            return rvPoint
    
    
        def CalcDerivative(self, parameter = 0.5):
            parameter2 = parameter * parameter
    
            rvPoint = self.coeff1 + self.coeff2 * parameter * 2.0 + self.coeff3 * parameter2 * 3.0
    
            return rvPoint
    
    
        def CalcLength(self, nrSamples = 2):
            nrSamplesFloat = float(nrSamples)
            rvLength = 0.0
            for iSample in range(nrSamples):
                par1 = float(iSample) / nrSamplesFloat
                par2 = float(iSample + 1) / nrSamplesFloat
    
                point1 = self.CalcPoint(parameter = par1)
                point2 = self.CalcPoint(parameter = par2)
                diff12 = point1 - point2
    
                rvLength += diff12.magnitude
    
            return rvLength
    
    
        #http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
        def CalcSplitPoint(self, parameter = 0.5):
            par1min = 1.0 - parameter
    
            bez00 = self.ctrlPnt0
            bez01 = self.ctrlPnt1
            bez02 = self.ctrlPnt2
            bez03 = self.ctrlPnt3
    
            bez10 = bez00 * par1min + bez01 * parameter
            bez11 = bez01 * par1min + bez02 * parameter
            bez12 = bez02 * par1min + bez03 * parameter
    
            bez20 = bez10 * par1min + bez11 * parameter
            bez21 = bez11 * par1min + bez12 * parameter
    
            bez30 = bez20 * par1min + bez21 * parameter
    
            bezPoint1 = BezierPoint(self.bezierPoint1.handle_left, bez00, bez10)
            bezPointNew = BezierPoint(bez20, bez30, bez21)
            bezPoint2 = BezierPoint(bez12, bez03, self.bezierPoint2.handle_right)
    
            return [bezPoint1, bezPointNew, bezPoint2]
    
    
    class BezierSpline:
        @staticmethod
        def FromSegments(listSegments):
            rvSpline = BezierSpline(None)
    
            rvSpline.segments = listSegments
    
            return rvSpline
    
    
        def __init__(self, blenderBezierSpline):
            if not blenderBezierSpline is None:
                if blenderBezierSpline.type != 'BEZIER':
                    print("## ERROR:", "blenderBezierSpline.type != 'BEZIER'")
                    raise Exception("blenderBezierSpline.type != 'BEZIER'")
                if len(blenderBezierSpline.bezier_points) < 1:
                    if not blenderBezierSpline.use_cyclic_u:
                        print("## ERROR:", "len(blenderBezierSpline.bezier_points) < 1")
                        raise Exception("len(blenderBezierSpline.bezier_points) < 1")
    
            self.bezierSpline = blenderBezierSpline
    
            self.resolution = 12
            self.isCyclic = False
            if not self.bezierSpline is None:
                self.resolution = self.bezierSpline.resolution_u
                self.isCyclic = self.bezierSpline.use_cyclic_u
    
            self.segments = self.SetupSegments()
    
    
        def __getattr__(self, attrName):
            if attrName == "nrSegments":
                return len(self.segments)
    
            if attrName == "bezierPoints":
                rvList = []
    
                for seg in self.segments: rvList.append(seg.bezierPoint1)
                if not self.isCyclic: rvList.append(self.segments[-1].bezierPoint2)
    
                return rvList
    
            if attrName == "resolutionPerSegment":
                try: rvResPS = int(self.resolution / self.nrSegments)
                except: rvResPS = 2
                if rvResPS < 2: rvResPS = 2
    
                return rvResPS
    
            if attrName == "length":
                return self.CalcLength()
    
            return None
    
    
        def SetupSegments(self):
            rvSegments = []
            if self.bezierSpline is None: return rvSegments
    
            nrBezierPoints = len(self.bezierSpline.bezier_points)
            for iBezierPoint in range(nrBezierPoints - 1):
                bezierPoint1 = self.bezierSpline.bezier_points[iBezierPoint]
                bezierPoint2 = self.bezierSpline.bezier_points[iBezierPoint + 1]
                rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
            if self.isCyclic:
                bezierPoint1 = self.bezierSpline.bezier_points[-1]
                bezierPoint2 = self.bezierSpline.bezier_points[0]
                rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
    
            return rvSegments
    
    
        def UpdateSegments(self, newSegments):
            prevNrSegments = len(self.segments)
            diffNrSegments = len(newSegments) - prevNrSegments
            if diffNrSegments > 0:
                newBezierPoints = []
                for segment in newSegments: newBezierPoints.append(segment.bezierPoint1)
                if not self.isCyclic: newBezierPoints.append(newSegments[-1].bezierPoint2)
    
                self.bezierSpline.bezier_points.add(diffNrSegments)
    
                for i, bezPoint in enumerate(newBezierPoints):
                    blBezPoint = self.bezierSpline.bezier_points[i]
    
                    blBezPoint.tilt = 0
                    blBezPoint.radius = 1.0
    
                    blBezPoint.handle_left_type = 'FREE'
                    blBezPoint.handle_left = bezPoint.handle_left
                    blBezPoint.co = bezPoint.co
                    blBezPoint.handle_right_type = 'FREE'
                    blBezPoint.handle_right = bezPoint.handle_right
    
                self.segments = newSegments
            else:
                print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
    
    
        def Reversed(self):
            revSegments = []
    
            for iSeg in reversed(range(self.nrSegments)): revSegments.append(self.segments[iSeg].Reversed())
    
            rvSpline = BezierSpline.FromSegments(revSegments)
            rvSpline.resolution = self.resolution
            rvSpline.isCyclic = self.isCyclic
    
            return rvSpline
    
    
        def Reverse(self):
            revSegments = []
    
            for iSeg in reversed(range(self.nrSegments)):
                self.segments[iSeg].Reverse()
                revSegments.append(self.segments[iSeg])
    
            self.segments = revSegments
    
    
        def CalcDivideResolution(self, segment, parameter):
            if not segment in self.segments:
                print("### WARNING: InsertPoint(): not segment in self.segments")
                return None
    
            iSeg = self.segments.index(segment)
            dPar = 1.0 / self.nrSegments
            splinePar = dPar * (parameter + float(iSeg))
    
            res1 = int(splinePar * self.resolution)
            if res1 < 2:
                print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1, "-- setting it to 2")
                res1 = 2
    
            res2 = int((1.0 - splinePar) * self.resolution)
            if res2 < 2:
                print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2, "-- setting it to 2")
                res2 = 2
    
            return [res1, res2]
            # return [self.resolution, self.resolution]
    
    
        def CalcPoint(self, parameter):
            nrSegs = self.nrSegments
    
            segmentIndex = int(nrSegs * parameter)
            if segmentIndex < 0: segmentIndex = 0
            if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
    
            segmentParameter = nrSegs * parameter - segmentIndex
            if segmentParameter < 0.0: segmentParameter = 0.0
            if segmentParameter > 1.0: segmentParameter = 1.0
    
            return self.segments[segmentIndex].CalcPoint(parameter = segmentParameter)
    
    
        def CalcDerivative(self, parameter):
            nrSegs = self.nrSegments
    
            segmentIndex = int(nrSegs * parameter)
            if segmentIndex < 0: segmentIndex = 0
            if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
    
            segmentParameter = nrSegs * parameter - segmentIndex
            if segmentParameter < 0.0: segmentParameter = 0.0
            if segmentParameter > 1.0: segmentParameter = 1.0
    
            return self.segments[segmentIndex].CalcDerivative(parameter = segmentParameter)
    
    
        def InsertPoint(self, segment, parameter):
            if not segment in self.segments:
                print("### WARNING: InsertPoint(): not segment in self.segments")
                return
            iSeg = self.segments.index(segment)
            nrSegments = len(self.segments)
    
            splitPoints = segment.CalcSplitPoint(parameter = parameter)
            bezPoint1 = splitPoints[0]
            bezPointNew = splitPoints[1]
            bezPoint2 = splitPoints[2]
    
            segment.bezierPoint1.handle_right = bezPoint1.handle_right
            segment.bezierPoint2 = bezPointNew
    
            if iSeg < (nrSegments - 1):
                nextSeg = self.segments[iSeg + 1]
                nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
            else:
                if self.isCyclic:
                    nextSeg = self.segments[0]
                    nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
    
    
            newSeg = BezierSegment(bezPointNew, bezPoint2)
            self.segments.insert(iSeg + 1, newSeg)
    
    
        def Split(self, segment, parameter):
            if not segment in self.segments:
                print("### WARNING: InsertPoint(): not segment in self.segments")
                return None
            iSeg = self.segments.index(segment)
            nrSegments = len(self.segments)
    
            splitPoints = segment.CalcSplitPoint(parameter = parameter)
            bezPoint1 = splitPoints[0]
            bezPointNew = splitPoints[1]
            bezPoint2 = splitPoints[2]
    
    
            newSpline1Segments = []
            for iSeg1 in range(iSeg): newSpline1Segments.append(self.segments[iSeg1])
            if len(newSpline1Segments) > 0: newSpline1Segments[-1].bezierPoint2.handle_right = bezPoint1.handle_right
            newSpline1Segments.append(BezierSegment(bezPoint1, bezPointNew))
    
            newSpline2Segments = []
            newSpline2Segments.append(BezierSegment(bezPointNew, bezPoint2))
            for iSeg2 in range(iSeg + 1, nrSegments): newSpline2Segments.append(self.segments[iSeg2])
            if len(newSpline2Segments) > 1: newSpline2Segments[1].bezierPoint1.handle_left = newSpline2Segments[0].bezierPoint2.handle_left
    
    
            newSpline1 = BezierSpline.FromSegments(newSpline1Segments)
            newSpline2 = BezierSpline.FromSegments(newSpline2Segments)
    
            return [newSpline1, newSpline2]
    
    
    
        def Join(self, spline2, mode = 'At_midpoint'):
            if mode == 'At_midpoint':
    
                self.JoinAtMidpoint(spline2)
                return
    
    
            if mode == 'Insert_segment':
    
                self.JoinInsertSegment(spline2)
                return
    
            print("### ERROR: Join(): unknown mode:", mode)
    
    
        def JoinAtMidpoint(self, spline2):
            bezPoint1 = self.segments[-1].bezierPoint2
            bezPoint2 = spline2.segments[0].bezierPoint1
    
            mpHandleLeft = bezPoint1.handle_left.copy()
            mpCo = (bezPoint1.co + bezPoint2.co) * 0.5
            mpHandleRight = bezPoint2.handle_right.copy()
            mpBezPoint = BezierPoint(mpHandleLeft, mpCo, mpHandleRight)
    
            self.segments[-1].bezierPoint2 = mpBezPoint
            spline2.segments[0].bezierPoint1 = mpBezPoint
            for seg2 in spline2.segments: self.segments.append(seg2)
    
            self.resolution += spline2.resolution
            self.isCyclic = False    # is this ok?
    
    
        def JoinInsertSegment(self, spline2):
            self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1))
            for seg2 in spline2.segments: self.segments.append(seg2)
    
    
            self.resolution += spline2.resolution    # extra segment will usually be short -- impact on resolution negligible
    
    
            self.isCyclic = False    # is this ok?
    
    
        def RefreshInScene(self):
            bezierPoints = self.bezierPoints
    
            currNrBezierPoints = len(self.bezierSpline.bezier_points)
            diffNrBezierPoints = len(bezierPoints) - currNrBezierPoints
            if diffNrBezierPoints > 0: self.bezierSpline.bezier_points.add(diffNrBezierPoints)
    
            for i, bezPoint in enumerate(bezierPoints):
                blBezPoint = self.bezierSpline.bezier_points[i]
    
                blBezPoint.tilt = 0
                blBezPoint.radius = 1.0
    
                blBezPoint.handle_left_type = 'FREE'
                blBezPoint.handle_left = bezPoint.handle_left
                blBezPoint.co = bezPoint.co
                blBezPoint.handle_right_type = 'FREE'
                blBezPoint.handle_right = bezPoint.handle_right
    
            self.bezierSpline.use_cyclic_u = self.isCyclic
            self.bezierSpline.resolution_u = self.resolution
    
    
        def CalcLength(self):
            try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
            except: nrSamplesPerSegment = 2
            if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
    
            rvLength = 0.0
            for segment in self.segments:
                rvLength += segment.CalcLength(nrSamples = nrSamplesPerSegment)
    
            return rvLength
    
    
        def GetLengthIsSmallerThan(self, threshold):
            try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
            except: nrSamplesPerSegment = 2
            if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
    
            length = 0.0
            for segment in self.segments:
                length += segment.CalcLength(nrSamples = nrSamplesPerSegment)
                if not length < threshold: return False
    
            return True
    
    
    class Curve:
        def __init__(self, blenderCurve):
            self.curve = blenderCurve
            self.curveData = blenderCurve.data
    
            self.splines = self.SetupSplines()
    
    
        def __getattr__(self, attrName):
            if attrName == "nrSplines":
                return len(self.splines)
    
            if attrName == "length":
                return self.CalcLength()
    
            if attrName == "worldMatrix":
                return self.curve.matrix_world
    
            if attrName == "location":
                return self.curve.location
    
            return None
    
    
        def SetupSplines(self):
            rvSplines = []
            for spline in self.curveData.splines:
                if spline.type != 'BEZIER':
                    print("## WARNING: only bezier splines are supported, atm; other types are ignored")
                    continue
    
                try: newSpline = BezierSpline(spline)
                except:
                    print("## EXCEPTION: newSpline = BezierSpline(spline)")
                    continue
    
                rvSplines.append(newSpline)
    
            return rvSplines
    
    
        def RebuildInScene(self):
            self.curveData.splines.clear()
    
            for spline in self.splines:
                blSpline = self.curveData.splines.new('BEZIER')
                blSpline.use_cyclic_u = spline.isCyclic
                blSpline.resolution_u = spline.resolution
    
                bezierPoints = []
                for segment in spline.segments: bezierPoints.append(segment.bezierPoint1)
                if not spline.isCyclic: bezierPoints.append(spline.segments[-1].bezierPoint2)
                #else: print("????", "spline.isCyclic")
    
                nrBezierPoints = len(bezierPoints)
                blSpline.bezier_points.add(nrBezierPoints - 1)
    
                for i, blBezPoint in enumerate(blSpline.bezier_points):
                    bezPoint = bezierPoints[i]
    
                    blBezPoint.tilt = 0
                    blBezPoint.radius = 1.0
    
                    blBezPoint.handle_left_type = 'FREE'
                    blBezPoint.handle_left = bezPoint.handle_left
                    blBezPoint.co = bezPoint.co
                    blBezPoint.handle_right_type = 'FREE'
                    blBezPoint.handle_right = bezPoint.handle_right
    
    
        def CalcLength(self):
            rvLength = 0.0
            for spline in self.splines:
                rvLength += spline.length
    
            return rvLength
    
    
        def RemoveShortSplines(self, threshold):
            splinesToRemove = []
    
            for spline in self.splines:
                if spline.GetLengthIsSmallerThan(threshold): splinesToRemove.append(spline)
    
            for spline in splinesToRemove: self.splines.remove(spline)
    
            return len(splinesToRemove)
    
    
        def JoinNeighbouringSplines(self, startEnd, threshold, mode):
            nrJoins = 0
    
            while True:
                firstPair = self.JoinGetFirstPair(startEnd, threshold)
                if firstPair is None: break
    
                firstPair[0].Join(firstPair[1], mode)
                self.splines.remove(firstPair[1])
    
                nrJoins += 1
    
            return nrJoins
    
    
        def JoinGetFirstPair(self, startEnd, threshold):
            nrSplines = len(self.splines)
    
            if startEnd:
                for iCurrentSpline in range(nrSplines):
                    currentSpline = self.splines[iCurrentSpline]
    
                    for iNextSpline in range(iCurrentSpline + 1, nrSplines):
                        nextSpline = self.splines[iNextSpline]
    
                        currEndPoint = currentSpline.segments[-1].bezierPoint2.co
                        nextStartPoint = nextSpline.segments[0].bezierPoint1.co
    
                        if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
    
    
                        nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
                        currStartPoint = currentSpline.segments[0].bezierPoint1.co
    
                        if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
    
    
                return None
            else:
                for iCurrentSpline in range(nrSplines):
                    currentSpline = self.splines[iCurrentSpline]
    
                    for iNextSpline in range(iCurrentSpline + 1, nrSplines):
                        nextSpline = self.splines[iNextSpline]
    
                        currEndPoint = currentSpline.segments[-1].bezierPoint2.co
                        nextStartPoint = nextSpline.segments[0].bezierPoint1.co
    
                        if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
    
    
                        nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
                        currStartPoint = currentSpline.segments[0].bezierPoint1.co
    
                        if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
    
                        if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold):
    
                            nextSpline.Reverse()
                            #print("## ", "nextSpline.Reverse()")
                            return [currentSpline, nextSpline]
    
    
                        if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold):
    
                            currentSpline.Reverse()
                            #print("## ", "currentSpline.Reverse()")
                            return [currentSpline, nextSpline]
    
                return None