Cleanup: remove unnecessary use of addon_utils to access the FBX version
[blender-addons.git] / curve_tools / curves.py
blobd818d2e101ba3b8a5759b299d8aea8e71b175f09
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 from . import mathematics
7 import bpy
10 class BezierPoint:
11 @staticmethod
12 def FromBlenderBezierPoint(blenderBezierPoint):
13 return BezierPoint(blenderBezierPoint.handle_left, blenderBezierPoint.co, blenderBezierPoint.handle_right)
16 def __init__(self, handle_left, co, handle_right):
17 self.handle_left = handle_left
18 self.co = co
19 self.handle_right = handle_right
22 def Copy(self):
23 return BezierPoint(self.handle_left.copy(), self.co.copy(), self.handle_right.copy())
25 def Reversed(self):
26 return BezierPoint(self.handle_right, self.co, self.handle_left)
28 def Reverse(self):
29 tmp = self.handle_left
30 self.handle_left = self.handle_right
31 self.handle_right = tmp
34 class BezierSegment:
35 @staticmethod
36 def FromBlenderBezierPoints(blenderBezierPoint1, blenderBezierPoint2):
37 bp1 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint1)
38 bp2 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint2)
40 return BezierSegment(bp1, bp2)
43 def Copy(self):
44 return BezierSegment(self.bezierPoint1.Copy(), self.bezierPoint2.Copy())
46 def Reversed(self):
47 return BezierSegment(self.bezierPoint2.Reversed(), self.bezierPoint1.Reversed())
49 def Reverse(self):
50 # make a copy, otherwise neighboring segment may be affected
51 tmp = self.bezierPoint1.Copy()
52 self.bezierPoint1 = self.bezierPoint2.Copy()
53 self.bezierPoint2 = tmp
54 self.bezierPoint1.Reverse()
55 self.bezierPoint2.Reverse()
58 def __init__(self, bezierPoint1, bezierPoint2):
59 # bpy.types.BezierSplinePoint
60 # ## NOTE/TIP: copy() helps with repeated (intersection) action -- ??
61 self.bezierPoint1 = bezierPoint1.Copy()
62 self.bezierPoint2 = bezierPoint2.Copy()
64 self.ctrlPnt0 = self.bezierPoint1.co
65 self.ctrlPnt1 = self.bezierPoint1.handle_right
66 self.ctrlPnt2 = self.bezierPoint2.handle_left
67 self.ctrlPnt3 = self.bezierPoint2.co
69 self.coeff0 = self.ctrlPnt0
70 self.coeff1 = self.ctrlPnt0 * (-3.0) + self.ctrlPnt1 * (+3.0)
71 self.coeff2 = self.ctrlPnt0 * (+3.0) + self.ctrlPnt1 * (-6.0) + self.ctrlPnt2 * (+3.0)
72 self.coeff3 = self.ctrlPnt0 * (-1.0) + self.ctrlPnt1 * (+3.0) + self.ctrlPnt2 * (-3.0) + self.ctrlPnt3
75 def CalcPoint(self, parameter = 0.5):
76 parameter2 = parameter * parameter
77 parameter3 = parameter * parameter2
79 rvPoint = self.coeff0 + self.coeff1 * parameter + self.coeff2 * parameter2 + self.coeff3 * parameter3
81 return rvPoint
84 def CalcDerivative(self, parameter = 0.5):
85 parameter2 = parameter * parameter
87 rvPoint = self.coeff1 + self.coeff2 * parameter * 2.0 + self.coeff3 * parameter2 * 3.0
89 return rvPoint
92 def CalcLength(self, nrSamples = 2):
93 nrSamplesFloat = float(nrSamples)
94 rvLength = 0.0
95 for iSample in range(nrSamples):
96 par1 = float(iSample) / nrSamplesFloat
97 par2 = float(iSample + 1) / nrSamplesFloat
99 point1 = self.CalcPoint(parameter = par1)
100 point2 = self.CalcPoint(parameter = par2)
101 diff12 = point1 - point2
103 rvLength += diff12.magnitude
105 return rvLength
108 #http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
109 def CalcSplitPoint(self, parameter = 0.5):
110 par1min = 1.0 - parameter
112 bez00 = self.ctrlPnt0
113 bez01 = self.ctrlPnt1
114 bez02 = self.ctrlPnt2
115 bez03 = self.ctrlPnt3
117 bez10 = bez00 * par1min + bez01 * parameter
118 bez11 = bez01 * par1min + bez02 * parameter
119 bez12 = bez02 * par1min + bez03 * parameter
121 bez20 = bez10 * par1min + bez11 * parameter
122 bez21 = bez11 * par1min + bez12 * parameter
124 bez30 = bez20 * par1min + bez21 * parameter
126 bezPoint1 = BezierPoint(self.bezierPoint1.handle_left, bez00, bez10)
127 bezPointNew = BezierPoint(bez20, bez30, bez21)
128 bezPoint2 = BezierPoint(bez12, bez03, self.bezierPoint2.handle_right)
130 return [bezPoint1, bezPointNew, bezPoint2]
133 class BezierSpline:
134 @staticmethod
135 def FromSegments(listSegments):
136 rvSpline = BezierSpline(None)
138 rvSpline.segments = listSegments
140 return rvSpline
143 def __init__(self, blenderBezierSpline):
144 if not blenderBezierSpline is None:
145 if blenderBezierSpline.type != 'BEZIER':
146 print("## ERROR:", "blenderBezierSpline.type != 'BEZIER'")
147 raise Exception("blenderBezierSpline.type != 'BEZIER'")
148 if len(blenderBezierSpline.bezier_points) < 1:
149 if not blenderBezierSpline.use_cyclic_u:
150 print("## ERROR:", "len(blenderBezierSpline.bezier_points) < 1")
151 raise Exception("len(blenderBezierSpline.bezier_points) < 1")
153 self.bezierSpline = blenderBezierSpline
155 self.resolution = 12
156 self.isCyclic = False
157 if not self.bezierSpline is None:
158 self.resolution = self.bezierSpline.resolution_u
159 self.isCyclic = self.bezierSpline.use_cyclic_u
161 self.segments = self.SetupSegments()
164 def __getattr__(self, attrName):
165 if attrName == "nrSegments":
166 return len(self.segments)
168 if attrName == "bezierPoints":
169 rvList = []
171 for seg in self.segments: rvList.append(seg.bezierPoint1)
172 if not self.isCyclic: rvList.append(self.segments[-1].bezierPoint2)
174 return rvList
176 if attrName == "resolutionPerSegment":
177 try: rvResPS = int(self.resolution / self.nrSegments)
178 except: rvResPS = 2
179 if rvResPS < 2: rvResPS = 2
181 return rvResPS
183 if attrName == "length":
184 return self.CalcLength()
186 return None
189 def SetupSegments(self):
190 rvSegments = []
191 if self.bezierSpline is None: return rvSegments
193 nrBezierPoints = len(self.bezierSpline.bezier_points)
194 for iBezierPoint in range(nrBezierPoints - 1):
195 bezierPoint1 = self.bezierSpline.bezier_points[iBezierPoint]
196 bezierPoint2 = self.bezierSpline.bezier_points[iBezierPoint + 1]
197 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
198 if self.isCyclic:
199 bezierPoint1 = self.bezierSpline.bezier_points[-1]
200 bezierPoint2 = self.bezierSpline.bezier_points[0]
201 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
203 return rvSegments
206 def UpdateSegments(self, newSegments):
207 prevNrSegments = len(self.segments)
208 diffNrSegments = len(newSegments) - prevNrSegments
209 if diffNrSegments > 0:
210 newBezierPoints = []
211 for segment in newSegments: newBezierPoints.append(segment.bezierPoint1)
212 if not self.isCyclic: newBezierPoints.append(newSegments[-1].bezierPoint2)
214 self.bezierSpline.bezier_points.add(diffNrSegments)
216 for i, bezPoint in enumerate(newBezierPoints):
217 blBezPoint = self.bezierSpline.bezier_points[i]
219 blBezPoint.tilt = 0
220 blBezPoint.radius = 1.0
222 blBezPoint.handle_left_type = 'FREE'
223 blBezPoint.handle_left = bezPoint.handle_left
224 blBezPoint.co = bezPoint.co
225 blBezPoint.handle_right_type = 'FREE'
226 blBezPoint.handle_right = bezPoint.handle_right
228 self.segments = newSegments
229 else:
230 print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
233 def Reversed(self):
234 revSegments = []
236 for iSeg in reversed(range(self.nrSegments)): revSegments.append(self.segments[iSeg].Reversed())
238 rvSpline = BezierSpline.FromSegments(revSegments)
239 rvSpline.resolution = self.resolution
240 rvSpline.isCyclic = self.isCyclic
242 return rvSpline
245 def Reverse(self):
246 revSegments = []
248 for iSeg in reversed(range(self.nrSegments)):
249 self.segments[iSeg].Reverse()
250 revSegments.append(self.segments[iSeg])
252 self.segments = revSegments
255 def CalcDivideResolution(self, segment, parameter):
256 if not segment in self.segments:
257 print("### WARNING: InsertPoint(): not segment in self.segments")
258 return None
260 iSeg = self.segments.index(segment)
261 dPar = 1.0 / self.nrSegments
262 splinePar = dPar * (parameter + float(iSeg))
264 res1 = int(splinePar * self.resolution)
265 if res1 < 2:
266 print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1, "-- setting it to 2")
267 res1 = 2
269 res2 = int((1.0 - splinePar) * self.resolution)
270 if res2 < 2:
271 print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2, "-- setting it to 2")
272 res2 = 2
274 return [res1, res2]
275 # return [self.resolution, self.resolution]
278 def CalcPoint(self, parameter):
279 nrSegs = self.nrSegments
281 segmentIndex = int(nrSegs * parameter)
282 if segmentIndex < 0: segmentIndex = 0
283 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
285 segmentParameter = nrSegs * parameter - segmentIndex
286 if segmentParameter < 0.0: segmentParameter = 0.0
287 if segmentParameter > 1.0: segmentParameter = 1.0
289 return self.segments[segmentIndex].CalcPoint(parameter = segmentParameter)
292 def CalcDerivative(self, parameter):
293 nrSegs = self.nrSegments
295 segmentIndex = int(nrSegs * parameter)
296 if segmentIndex < 0: segmentIndex = 0
297 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
299 segmentParameter = nrSegs * parameter - segmentIndex
300 if segmentParameter < 0.0: segmentParameter = 0.0
301 if segmentParameter > 1.0: segmentParameter = 1.0
303 return self.segments[segmentIndex].CalcDerivative(parameter = segmentParameter)
306 def InsertPoint(self, segment, parameter):
307 if not segment in self.segments:
308 print("### WARNING: InsertPoint(): not segment in self.segments")
309 return
310 iSeg = self.segments.index(segment)
311 nrSegments = len(self.segments)
313 splitPoints = segment.CalcSplitPoint(parameter = parameter)
314 bezPoint1 = splitPoints[0]
315 bezPointNew = splitPoints[1]
316 bezPoint2 = splitPoints[2]
318 segment.bezierPoint1.handle_right = bezPoint1.handle_right
319 segment.bezierPoint2 = bezPointNew
321 if iSeg < (nrSegments - 1):
322 nextSeg = self.segments[iSeg + 1]
323 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
324 else:
325 if self.isCyclic:
326 nextSeg = self.segments[0]
327 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
330 newSeg = BezierSegment(bezPointNew, bezPoint2)
331 self.segments.insert(iSeg + 1, newSeg)
334 def Split(self, segment, parameter):
335 if not segment in self.segments:
336 print("### WARNING: InsertPoint(): not segment in self.segments")
337 return None
338 iSeg = self.segments.index(segment)
339 nrSegments = len(self.segments)
341 splitPoints = segment.CalcSplitPoint(parameter = parameter)
342 bezPoint1 = splitPoints[0]
343 bezPointNew = splitPoints[1]
344 bezPoint2 = splitPoints[2]
347 newSpline1Segments = []
348 for iSeg1 in range(iSeg): newSpline1Segments.append(self.segments[iSeg1])
349 if len(newSpline1Segments) > 0: newSpline1Segments[-1].bezierPoint2.handle_right = bezPoint1.handle_right
350 newSpline1Segments.append(BezierSegment(bezPoint1, bezPointNew))
352 newSpline2Segments = []
353 newSpline2Segments.append(BezierSegment(bezPointNew, bezPoint2))
354 for iSeg2 in range(iSeg + 1, nrSegments): newSpline2Segments.append(self.segments[iSeg2])
355 if len(newSpline2Segments) > 1: newSpline2Segments[1].bezierPoint1.handle_left = newSpline2Segments[0].bezierPoint2.handle_left
358 newSpline1 = BezierSpline.FromSegments(newSpline1Segments)
359 newSpline2 = BezierSpline.FromSegments(newSpline2Segments)
361 return [newSpline1, newSpline2]
364 def Join(self, spline2, mode = 'At_midpoint'):
365 if mode == 'At_midpoint':
366 self.JoinAtMidpoint(spline2)
367 return
369 if mode == 'Insert_segment':
370 self.JoinInsertSegment(spline2)
371 return
373 print("### ERROR: Join(): unknown mode:", mode)
376 def JoinAtMidpoint(self, spline2):
377 bezPoint1 = self.segments[-1].bezierPoint2
378 bezPoint2 = spline2.segments[0].bezierPoint1
380 mpHandleLeft = bezPoint1.handle_left.copy()
381 mpCo = (bezPoint1.co + bezPoint2.co) * 0.5
382 mpHandleRight = bezPoint2.handle_right.copy()
383 mpBezPoint = BezierPoint(mpHandleLeft, mpCo, mpHandleRight)
385 self.segments[-1].bezierPoint2 = mpBezPoint
386 spline2.segments[0].bezierPoint1 = mpBezPoint
387 for seg2 in spline2.segments: self.segments.append(seg2)
389 self.resolution += spline2.resolution
390 self.isCyclic = False # is this ok?
393 def JoinInsertSegment(self, spline2):
394 self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1))
395 for seg2 in spline2.segments: self.segments.append(seg2)
397 self.resolution += spline2.resolution # extra segment will usually be short -- impact on resolution negligible
399 self.isCyclic = False # is this ok?
402 def RefreshInScene(self):
403 bezierPoints = self.bezierPoints
405 currNrBezierPoints = len(self.bezierSpline.bezier_points)
406 diffNrBezierPoints = len(bezierPoints) - currNrBezierPoints
407 if diffNrBezierPoints > 0: self.bezierSpline.bezier_points.add(diffNrBezierPoints)
409 for i, bezPoint in enumerate(bezierPoints):
410 blBezPoint = self.bezierSpline.bezier_points[i]
412 blBezPoint.tilt = 0
413 blBezPoint.radius = 1.0
415 blBezPoint.handle_left_type = 'FREE'
416 blBezPoint.handle_left = bezPoint.handle_left
417 blBezPoint.co = bezPoint.co
418 blBezPoint.handle_right_type = 'FREE'
419 blBezPoint.handle_right = bezPoint.handle_right
421 self.bezierSpline.use_cyclic_u = self.isCyclic
422 self.bezierSpline.resolution_u = self.resolution
425 def CalcLength(self):
426 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
427 except: nrSamplesPerSegment = 2
428 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
430 rvLength = 0.0
431 for segment in self.segments:
432 rvLength += segment.CalcLength(nrSamples = nrSamplesPerSegment)
434 return rvLength
437 def GetLengthIsSmallerThan(self, threshold):
438 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
439 except: nrSamplesPerSegment = 2
440 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
442 length = 0.0
443 for segment in self.segments:
444 length += segment.CalcLength(nrSamples = nrSamplesPerSegment)
445 if not length < threshold: return False
447 return True
450 class Curve:
451 def __init__(self, blenderCurve):
452 self.curve = blenderCurve
453 self.curveData = blenderCurve.data
455 self.splines = self.SetupSplines()
458 def __getattr__(self, attrName):
459 if attrName == "nrSplines":
460 return len(self.splines)
462 if attrName == "length":
463 return self.CalcLength()
465 if attrName == "worldMatrix":
466 return self.curve.matrix_world
468 if attrName == "location":
469 return self.curve.location
471 return None
474 def SetupSplines(self):
475 rvSplines = []
476 for spline in self.curveData.splines:
477 if spline.type != 'BEZIER':
478 print("## WARNING: only bezier splines are supported, atm; other types are ignored")
479 continue
481 try: newSpline = BezierSpline(spline)
482 except:
483 print("## EXCEPTION: newSpline = BezierSpline(spline)")
484 continue
486 rvSplines.append(newSpline)
488 return rvSplines
491 def RebuildInScene(self):
492 self.curveData.splines.clear()
494 for spline in self.splines:
495 blSpline = self.curveData.splines.new('BEZIER')
496 blSpline.use_cyclic_u = spline.isCyclic
497 blSpline.resolution_u = spline.resolution
499 bezierPoints = []
500 for segment in spline.segments: bezierPoints.append(segment.bezierPoint1)
501 if not spline.isCyclic: bezierPoints.append(spline.segments[-1].bezierPoint2)
502 #else: print("????", "spline.isCyclic")
504 nrBezierPoints = len(bezierPoints)
505 blSpline.bezier_points.add(nrBezierPoints - 1)
507 for i, blBezPoint in enumerate(blSpline.bezier_points):
508 bezPoint = bezierPoints[i]
510 blBezPoint.tilt = 0
511 blBezPoint.radius = 1.0
513 blBezPoint.handle_left_type = 'FREE'
514 blBezPoint.handle_left = bezPoint.handle_left
515 blBezPoint.co = bezPoint.co
516 blBezPoint.handle_right_type = 'FREE'
517 blBezPoint.handle_right = bezPoint.handle_right
520 def CalcLength(self):
521 rvLength = 0.0
522 for spline in self.splines:
523 rvLength += spline.length
525 return rvLength
528 def RemoveShortSplines(self, threshold):
529 splinesToRemove = []
531 for spline in self.splines:
532 if spline.GetLengthIsSmallerThan(threshold): splinesToRemove.append(spline)
534 for spline in splinesToRemove: self.splines.remove(spline)
536 return len(splinesToRemove)
539 def JoinNeighbouringSplines(self, startEnd, threshold, mode):
540 nrJoins = 0
542 while True:
543 firstPair = self.JoinGetFirstPair(startEnd, threshold)
544 if firstPair is None: break
546 firstPair[0].Join(firstPair[1], mode)
547 self.splines.remove(firstPair[1])
549 nrJoins += 1
551 return nrJoins
554 def JoinGetFirstPair(self, startEnd, threshold):
555 nrSplines = len(self.splines)
557 if startEnd:
558 for iCurrentSpline in range(nrSplines):
559 currentSpline = self.splines[iCurrentSpline]
561 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
562 nextSpline = self.splines[iNextSpline]
564 currEndPoint = currentSpline.segments[-1].bezierPoint2.co
565 nextStartPoint = nextSpline.segments[0].bezierPoint1.co
566 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
568 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
569 currStartPoint = currentSpline.segments[0].bezierPoint1.co
570 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
572 return None
573 else:
574 for iCurrentSpline in range(nrSplines):
575 currentSpline = self.splines[iCurrentSpline]
577 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
578 nextSpline = self.splines[iNextSpline]
580 currEndPoint = currentSpline.segments[-1].bezierPoint2.co
581 nextStartPoint = nextSpline.segments[0].bezierPoint1.co
582 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
584 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
585 currStartPoint = currentSpline.segments[0].bezierPoint1.co
586 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
588 if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold):
589 nextSpline.Reverse()
590 #print("## ", "nextSpline.Reverse()")
591 return [currentSpline, nextSpline]
593 if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold):
594 currentSpline.Reverse()
595 #print("## ", "currentSpline.Reverse()")
596 return [currentSpline, nextSpline]
598 return None