Fix io_anim_camera error exporting cameras with quotes in their name
[blender-addons.git] / curve_tools / curves.py
blob8b5e2e7f89695db5abf76d97a710d2d7574aaef3
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 from . import mathematics
5 import bpy
8 class BezierPoint:
9 @staticmethod
10 def FromBlenderBezierPoint(blenderBezierPoint):
11 return BezierPoint(blenderBezierPoint.handle_left, blenderBezierPoint.co, blenderBezierPoint.handle_right)
14 def __init__(self, handle_left, co, handle_right):
15 self.handle_left = handle_left
16 self.co = co
17 self.handle_right = handle_right
20 def Copy(self):
21 return BezierPoint(self.handle_left.copy(), self.co.copy(), self.handle_right.copy())
23 def Reversed(self):
24 return BezierPoint(self.handle_right, self.co, self.handle_left)
26 def Reverse(self):
27 tmp = self.handle_left
28 self.handle_left = self.handle_right
29 self.handle_right = tmp
32 class BezierSegment:
33 @staticmethod
34 def FromBlenderBezierPoints(blenderBezierPoint1, blenderBezierPoint2):
35 bp1 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint1)
36 bp2 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint2)
38 return BezierSegment(bp1, bp2)
41 def Copy(self):
42 return BezierSegment(self.bezierPoint1.Copy(), self.bezierPoint2.Copy())
44 def Reversed(self):
45 return BezierSegment(self.bezierPoint2.Reversed(), self.bezierPoint1.Reversed())
47 def Reverse(self):
48 # make a copy, otherwise neighboring segment may be affected
49 tmp = self.bezierPoint1.Copy()
50 self.bezierPoint1 = self.bezierPoint2.Copy()
51 self.bezierPoint2 = tmp
52 self.bezierPoint1.Reverse()
53 self.bezierPoint2.Reverse()
56 def __init__(self, bezierPoint1, bezierPoint2):
57 # bpy.types.BezierSplinePoint
58 # ## NOTE/TIP: copy() helps with repeated (intersection) action -- ??
59 self.bezierPoint1 = bezierPoint1.Copy()
60 self.bezierPoint2 = bezierPoint2.Copy()
62 self.ctrlPnt0 = self.bezierPoint1.co
63 self.ctrlPnt1 = self.bezierPoint1.handle_right
64 self.ctrlPnt2 = self.bezierPoint2.handle_left
65 self.ctrlPnt3 = self.bezierPoint2.co
67 self.coeff0 = self.ctrlPnt0
68 self.coeff1 = self.ctrlPnt0 * (-3.0) + self.ctrlPnt1 * (+3.0)
69 self.coeff2 = self.ctrlPnt0 * (+3.0) + self.ctrlPnt1 * (-6.0) + self.ctrlPnt2 * (+3.0)
70 self.coeff3 = self.ctrlPnt0 * (-1.0) + self.ctrlPnt1 * (+3.0) + self.ctrlPnt2 * (-3.0) + self.ctrlPnt3
73 def CalcPoint(self, parameter = 0.5):
74 parameter2 = parameter * parameter
75 parameter3 = parameter * parameter2
77 rvPoint = self.coeff0 + self.coeff1 * parameter + self.coeff2 * parameter2 + self.coeff3 * parameter3
79 return rvPoint
82 def CalcDerivative(self, parameter = 0.5):
83 parameter2 = parameter * parameter
85 rvPoint = self.coeff1 + self.coeff2 * parameter * 2.0 + self.coeff3 * parameter2 * 3.0
87 return rvPoint
90 def CalcLength(self, nrSamples = 2):
91 nrSamplesFloat = float(nrSamples)
92 rvLength = 0.0
93 for iSample in range(nrSamples):
94 par1 = float(iSample) / nrSamplesFloat
95 par2 = float(iSample + 1) / nrSamplesFloat
97 point1 = self.CalcPoint(parameter = par1)
98 point2 = self.CalcPoint(parameter = par2)
99 diff12 = point1 - point2
101 rvLength += diff12.magnitude
103 return rvLength
106 #http://en.wikipedia.org/wiki/De_Casteljau's_algorithm
107 def CalcSplitPoint(self, parameter = 0.5):
108 par1min = 1.0 - parameter
110 bez00 = self.ctrlPnt0
111 bez01 = self.ctrlPnt1
112 bez02 = self.ctrlPnt2
113 bez03 = self.ctrlPnt3
115 bez10 = bez00 * par1min + bez01 * parameter
116 bez11 = bez01 * par1min + bez02 * parameter
117 bez12 = bez02 * par1min + bez03 * parameter
119 bez20 = bez10 * par1min + bez11 * parameter
120 bez21 = bez11 * par1min + bez12 * parameter
122 bez30 = bez20 * par1min + bez21 * parameter
124 bezPoint1 = BezierPoint(self.bezierPoint1.handle_left, bez00, bez10)
125 bezPointNew = BezierPoint(bez20, bez30, bez21)
126 bezPoint2 = BezierPoint(bez12, bez03, self.bezierPoint2.handle_right)
128 return [bezPoint1, bezPointNew, bezPoint2]
131 class BezierSpline:
132 @staticmethod
133 def FromSegments(listSegments):
134 rvSpline = BezierSpline(None)
136 rvSpline.segments = listSegments
138 return rvSpline
141 def __init__(self, blenderBezierSpline):
142 if not blenderBezierSpline is None:
143 if blenderBezierSpline.type != 'BEZIER':
144 print("## ERROR:", "blenderBezierSpline.type != 'BEZIER'")
145 raise Exception("blenderBezierSpline.type != 'BEZIER'")
146 if len(blenderBezierSpline.bezier_points) < 1:
147 if not blenderBezierSpline.use_cyclic_u:
148 print("## ERROR:", "len(blenderBezierSpline.bezier_points) < 1")
149 raise Exception("len(blenderBezierSpline.bezier_points) < 1")
151 self.bezierSpline = blenderBezierSpline
153 self.resolution = 12
154 self.isCyclic = False
155 if not self.bezierSpline is None:
156 self.resolution = self.bezierSpline.resolution_u
157 self.isCyclic = self.bezierSpline.use_cyclic_u
159 self.segments = self.SetupSegments()
162 def __getattr__(self, attrName):
163 if attrName == "nrSegments":
164 return len(self.segments)
166 if attrName == "bezierPoints":
167 rvList = []
169 for seg in self.segments: rvList.append(seg.bezierPoint1)
170 if not self.isCyclic: rvList.append(self.segments[-1].bezierPoint2)
172 return rvList
174 if attrName == "resolutionPerSegment":
175 try: rvResPS = int(self.resolution / self.nrSegments)
176 except: rvResPS = 2
177 if rvResPS < 2: rvResPS = 2
179 return rvResPS
181 if attrName == "length":
182 return self.CalcLength()
184 return None
187 def SetupSegments(self):
188 rvSegments = []
189 if self.bezierSpline is None: return rvSegments
191 nrBezierPoints = len(self.bezierSpline.bezier_points)
192 for iBezierPoint in range(nrBezierPoints - 1):
193 bezierPoint1 = self.bezierSpline.bezier_points[iBezierPoint]
194 bezierPoint2 = self.bezierSpline.bezier_points[iBezierPoint + 1]
195 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
196 if self.isCyclic:
197 bezierPoint1 = self.bezierSpline.bezier_points[-1]
198 bezierPoint2 = self.bezierSpline.bezier_points[0]
199 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2))
201 return rvSegments
204 def UpdateSegments(self, newSegments):
205 prevNrSegments = len(self.segments)
206 diffNrSegments = len(newSegments) - prevNrSegments
207 if diffNrSegments > 0:
208 newBezierPoints = []
209 for segment in newSegments: newBezierPoints.append(segment.bezierPoint1)
210 if not self.isCyclic: newBezierPoints.append(newSegments[-1].bezierPoint2)
212 self.bezierSpline.bezier_points.add(diffNrSegments)
214 for i, bezPoint in enumerate(newBezierPoints):
215 blBezPoint = self.bezierSpline.bezier_points[i]
217 blBezPoint.tilt = 0
218 blBezPoint.radius = 1.0
220 blBezPoint.handle_left_type = 'FREE'
221 blBezPoint.handle_left = bezPoint.handle_left
222 blBezPoint.co = bezPoint.co
223 blBezPoint.handle_right_type = 'FREE'
224 blBezPoint.handle_right = bezPoint.handle_right
226 self.segments = newSegments
227 else:
228 print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
231 def Reversed(self):
232 revSegments = []
234 for iSeg in reversed(range(self.nrSegments)): revSegments.append(self.segments[iSeg].Reversed())
236 rvSpline = BezierSpline.FromSegments(revSegments)
237 rvSpline.resolution = self.resolution
238 rvSpline.isCyclic = self.isCyclic
240 return rvSpline
243 def Reverse(self):
244 revSegments = []
246 for iSeg in reversed(range(self.nrSegments)):
247 self.segments[iSeg].Reverse()
248 revSegments.append(self.segments[iSeg])
250 self.segments = revSegments
253 def CalcDivideResolution(self, segment, parameter):
254 if not segment in self.segments:
255 print("### WARNING: InsertPoint(): not segment in self.segments")
256 return None
258 iSeg = self.segments.index(segment)
259 dPar = 1.0 / self.nrSegments
260 splinePar = dPar * (parameter + float(iSeg))
262 res1 = int(splinePar * self.resolution)
263 if res1 < 2:
264 print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1, "-- setting it to 2")
265 res1 = 2
267 res2 = int((1.0 - splinePar) * self.resolution)
268 if res2 < 2:
269 print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2, "-- setting it to 2")
270 res2 = 2
272 return [res1, res2]
273 # return [self.resolution, self.resolution]
276 def CalcPoint(self, parameter):
277 nrSegs = self.nrSegments
279 segmentIndex = int(nrSegs * parameter)
280 if segmentIndex < 0: segmentIndex = 0
281 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
283 segmentParameter = nrSegs * parameter - segmentIndex
284 if segmentParameter < 0.0: segmentParameter = 0.0
285 if segmentParameter > 1.0: segmentParameter = 1.0
287 return self.segments[segmentIndex].CalcPoint(parameter = segmentParameter)
290 def CalcDerivative(self, parameter):
291 nrSegs = self.nrSegments
293 segmentIndex = int(nrSegs * parameter)
294 if segmentIndex < 0: segmentIndex = 0
295 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1
297 segmentParameter = nrSegs * parameter - segmentIndex
298 if segmentParameter < 0.0: segmentParameter = 0.0
299 if segmentParameter > 1.0: segmentParameter = 1.0
301 return self.segments[segmentIndex].CalcDerivative(parameter = segmentParameter)
304 def InsertPoint(self, segment, parameter):
305 if not segment in self.segments:
306 print("### WARNING: InsertPoint(): not segment in self.segments")
307 return
308 iSeg = self.segments.index(segment)
309 nrSegments = len(self.segments)
311 splitPoints = segment.CalcSplitPoint(parameter = parameter)
312 bezPoint1 = splitPoints[0]
313 bezPointNew = splitPoints[1]
314 bezPoint2 = splitPoints[2]
316 segment.bezierPoint1.handle_right = bezPoint1.handle_right
317 segment.bezierPoint2 = bezPointNew
319 if iSeg < (nrSegments - 1):
320 nextSeg = self.segments[iSeg + 1]
321 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
322 else:
323 if self.isCyclic:
324 nextSeg = self.segments[0]
325 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left
328 newSeg = BezierSegment(bezPointNew, bezPoint2)
329 self.segments.insert(iSeg + 1, newSeg)
332 def Split(self, segment, parameter):
333 if not segment in self.segments:
334 print("### WARNING: InsertPoint(): not segment in self.segments")
335 return None
336 iSeg = self.segments.index(segment)
337 nrSegments = len(self.segments)
339 splitPoints = segment.CalcSplitPoint(parameter = parameter)
340 bezPoint1 = splitPoints[0]
341 bezPointNew = splitPoints[1]
342 bezPoint2 = splitPoints[2]
345 newSpline1Segments = []
346 for iSeg1 in range(iSeg): newSpline1Segments.append(self.segments[iSeg1])
347 if len(newSpline1Segments) > 0: newSpline1Segments[-1].bezierPoint2.handle_right = bezPoint1.handle_right
348 newSpline1Segments.append(BezierSegment(bezPoint1, bezPointNew))
350 newSpline2Segments = []
351 newSpline2Segments.append(BezierSegment(bezPointNew, bezPoint2))
352 for iSeg2 in range(iSeg + 1, nrSegments): newSpline2Segments.append(self.segments[iSeg2])
353 if len(newSpline2Segments) > 1: newSpline2Segments[1].bezierPoint1.handle_left = newSpline2Segments[0].bezierPoint2.handle_left
356 newSpline1 = BezierSpline.FromSegments(newSpline1Segments)
357 newSpline2 = BezierSpline.FromSegments(newSpline2Segments)
359 return [newSpline1, newSpline2]
362 def Join(self, spline2, mode = 'At_midpoint'):
363 if mode == 'At_midpoint':
364 self.JoinAtMidpoint(spline2)
365 return
367 if mode == 'Insert_segment':
368 self.JoinInsertSegment(spline2)
369 return
371 print("### ERROR: Join(): unknown mode:", mode)
374 def JoinAtMidpoint(self, spline2):
375 bezPoint1 = self.segments[-1].bezierPoint2
376 bezPoint2 = spline2.segments[0].bezierPoint1
378 mpHandleLeft = bezPoint1.handle_left.copy()
379 mpCo = (bezPoint1.co + bezPoint2.co) * 0.5
380 mpHandleRight = bezPoint2.handle_right.copy()
381 mpBezPoint = BezierPoint(mpHandleLeft, mpCo, mpHandleRight)
383 self.segments[-1].bezierPoint2 = mpBezPoint
384 spline2.segments[0].bezierPoint1 = mpBezPoint
385 for seg2 in spline2.segments: self.segments.append(seg2)
387 self.resolution += spline2.resolution
388 self.isCyclic = False # is this ok?
391 def JoinInsertSegment(self, spline2):
392 self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1))
393 for seg2 in spline2.segments: self.segments.append(seg2)
395 self.resolution += spline2.resolution # extra segment will usually be short -- impact on resolution negligible
397 self.isCyclic = False # is this ok?
400 def RefreshInScene(self):
401 bezierPoints = self.bezierPoints
403 currNrBezierPoints = len(self.bezierSpline.bezier_points)
404 diffNrBezierPoints = len(bezierPoints) - currNrBezierPoints
405 if diffNrBezierPoints > 0: self.bezierSpline.bezier_points.add(diffNrBezierPoints)
407 for i, bezPoint in enumerate(bezierPoints):
408 blBezPoint = self.bezierSpline.bezier_points[i]
410 blBezPoint.tilt = 0
411 blBezPoint.radius = 1.0
413 blBezPoint.handle_left_type = 'FREE'
414 blBezPoint.handle_left = bezPoint.handle_left
415 blBezPoint.co = bezPoint.co
416 blBezPoint.handle_right_type = 'FREE'
417 blBezPoint.handle_right = bezPoint.handle_right
419 self.bezierSpline.use_cyclic_u = self.isCyclic
420 self.bezierSpline.resolution_u = self.resolution
423 def CalcLength(self):
424 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
425 except: nrSamplesPerSegment = 2
426 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
428 rvLength = 0.0
429 for segment in self.segments:
430 rvLength += segment.CalcLength(nrSamples = nrSamplesPerSegment)
432 return rvLength
435 def GetLengthIsSmallerThan(self, threshold):
436 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments)
437 except: nrSamplesPerSegment = 2
438 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2
440 length = 0.0
441 for segment in self.segments:
442 length += segment.CalcLength(nrSamples = nrSamplesPerSegment)
443 if not length < threshold: return False
445 return True
448 class Curve:
449 def __init__(self, blenderCurve):
450 self.curve = blenderCurve
451 self.curveData = blenderCurve.data
453 self.splines = self.SetupSplines()
456 def __getattr__(self, attrName):
457 if attrName == "nrSplines":
458 return len(self.splines)
460 if attrName == "length":
461 return self.CalcLength()
463 if attrName == "worldMatrix":
464 return self.curve.matrix_world
466 if attrName == "location":
467 return self.curve.location
469 return None
472 def SetupSplines(self):
473 rvSplines = []
474 for spline in self.curveData.splines:
475 if spline.type != 'BEZIER':
476 print("## WARNING: only bezier splines are supported, atm; other types are ignored")
477 continue
479 try: newSpline = BezierSpline(spline)
480 except:
481 print("## EXCEPTION: newSpline = BezierSpline(spline)")
482 continue
484 rvSplines.append(newSpline)
486 return rvSplines
489 def RebuildInScene(self):
490 self.curveData.splines.clear()
492 for spline in self.splines:
493 blSpline = self.curveData.splines.new('BEZIER')
494 blSpline.use_cyclic_u = spline.isCyclic
495 blSpline.resolution_u = spline.resolution
497 bezierPoints = []
498 for segment in spline.segments: bezierPoints.append(segment.bezierPoint1)
499 if not spline.isCyclic: bezierPoints.append(spline.segments[-1].bezierPoint2)
500 #else: print("????", "spline.isCyclic")
502 nrBezierPoints = len(bezierPoints)
503 blSpline.bezier_points.add(nrBezierPoints - 1)
505 for i, blBezPoint in enumerate(blSpline.bezier_points):
506 bezPoint = bezierPoints[i]
508 blBezPoint.tilt = 0
509 blBezPoint.radius = 1.0
511 blBezPoint.handle_left_type = 'FREE'
512 blBezPoint.handle_left = bezPoint.handle_left
513 blBezPoint.co = bezPoint.co
514 blBezPoint.handle_right_type = 'FREE'
515 blBezPoint.handle_right = bezPoint.handle_right
518 def CalcLength(self):
519 rvLength = 0.0
520 for spline in self.splines:
521 rvLength += spline.length
523 return rvLength
526 def RemoveShortSplines(self, threshold):
527 splinesToRemove = []
529 for spline in self.splines:
530 if spline.GetLengthIsSmallerThan(threshold): splinesToRemove.append(spline)
532 for spline in splinesToRemove: self.splines.remove(spline)
534 return len(splinesToRemove)
537 def JoinNeighbouringSplines(self, startEnd, threshold, mode):
538 nrJoins = 0
540 while True:
541 firstPair = self.JoinGetFirstPair(startEnd, threshold)
542 if firstPair is None: break
544 firstPair[0].Join(firstPair[1], mode)
545 self.splines.remove(firstPair[1])
547 nrJoins += 1
549 return nrJoins
552 def JoinGetFirstPair(self, startEnd, threshold):
553 nrSplines = len(self.splines)
555 if startEnd:
556 for iCurrentSpline in range(nrSplines):
557 currentSpline = self.splines[iCurrentSpline]
559 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
560 nextSpline = self.splines[iNextSpline]
562 currEndPoint = currentSpline.segments[-1].bezierPoint2.co
563 nextStartPoint = nextSpline.segments[0].bezierPoint1.co
564 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
566 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
567 currStartPoint = currentSpline.segments[0].bezierPoint1.co
568 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
570 return None
571 else:
572 for iCurrentSpline in range(nrSplines):
573 currentSpline = self.splines[iCurrentSpline]
575 for iNextSpline in range(iCurrentSpline + 1, nrSplines):
576 nextSpline = self.splines[iNextSpline]
578 currEndPoint = currentSpline.segments[-1].bezierPoint2.co
579 nextStartPoint = nextSpline.segments[0].bezierPoint1.co
580 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline]
582 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co
583 currStartPoint = currentSpline.segments[0].bezierPoint1.co
584 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline]
586 if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold):
587 nextSpline.Reverse()
588 #print("## ", "nextSpline.Reverse()")
589 return [currentSpline, nextSpline]
591 if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold):
592 currentSpline.Reverse()
593 #print("## ", "currentSpline.Reverse()")
594 return [currentSpline, nextSpline]
596 return None