1 # SPDX-License-Identifier: GPL-2.0-or-later
3 from . import mathematics
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
17 self
.handle_right
= handle_right
21 return BezierPoint(self
.handle_left
.copy(), self
.co
.copy(), self
.handle_right
.copy())
24 return BezierPoint(self
.handle_right
, self
.co
, self
.handle_left
)
27 tmp
= self
.handle_left
28 self
.handle_left
= self
.handle_right
29 self
.handle_right
= tmp
34 def FromBlenderBezierPoints(blenderBezierPoint1
, blenderBezierPoint2
):
35 bp1
= BezierPoint
.FromBlenderBezierPoint(blenderBezierPoint1
)
36 bp2
= BezierPoint
.FromBlenderBezierPoint(blenderBezierPoint2
)
38 return BezierSegment(bp1
, bp2
)
42 return BezierSegment(self
.bezierPoint1
.Copy(), self
.bezierPoint2
.Copy())
45 return BezierSegment(self
.bezierPoint2
.Reversed(), self
.bezierPoint1
.Reversed())
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
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
90 def CalcLength(self
, nrSamples
= 2):
91 nrSamplesFloat
= float(nrSamples
)
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
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
]
133 def FromSegments(listSegments
):
134 rvSpline
= BezierSpline(None)
136 rvSpline
.segments
= listSegments
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
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":
169 for seg
in self
.segments
: rvList
.append(seg
.bezierPoint1
)
170 if not self
.isCyclic
: rvList
.append(self
.segments
[-1].bezierPoint2
)
174 if attrName
== "resolutionPerSegment":
175 try: rvResPS
= int(self
.resolution
/ self
.nrSegments
)
177 if rvResPS
< 2: rvResPS
= 2
181 if attrName
== "length":
182 return self
.CalcLength()
187 def SetupSegments(self
):
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
))
197 bezierPoint1
= self
.bezierSpline
.bezier_points
[-1]
198 bezierPoint2
= self
.bezierSpline
.bezier_points
[0]
199 rvSegments
.append(BezierSegment
.FromBlenderBezierPoints(bezierPoint1
, bezierPoint2
))
204 def UpdateSegments(self
, newSegments
):
205 prevNrSegments
= len(self
.segments
)
206 diffNrSegments
= len(newSegments
) - prevNrSegments
207 if diffNrSegments
> 0:
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
]
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
228 print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
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
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")
258 iSeg
= self
.segments
.index(segment
)
259 dPar
= 1.0 / self
.nrSegments
260 splinePar
= dPar
* (parameter
+ float(iSeg
))
262 res1
= int(splinePar
* self
.resolution
)
264 print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1
, "-- setting it to 2")
267 res2
= int((1.0 - splinePar
) * self
.resolution
)
269 print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2
, "-- setting it to 2")
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")
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
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")
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
)
367 if mode
== 'Insert_segment':
368 self
.JoinInsertSegment(spline2
)
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
]
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
429 for segment
in self
.segments
:
430 rvLength
+= segment
.CalcLength(nrSamples
= nrSamplesPerSegment
)
435 def GetLengthIsSmallerThan(self
, threshold
):
436 try: nrSamplesPerSegment
= int(self
.resolution
/ self
.nrSegments
)
437 except: nrSamplesPerSegment
= 2
438 if nrSamplesPerSegment
< 2: nrSamplesPerSegment
= 2
441 for segment
in self
.segments
:
442 length
+= segment
.CalcLength(nrSamples
= nrSamplesPerSegment
)
443 if not length
< threshold
: return False
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
472 def SetupSplines(self
):
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")
479 try: newSpline
= BezierSpline(spline
)
481 print("## EXCEPTION: newSpline = BezierSpline(spline)")
484 rvSplines
.append(newSpline
)
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
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
]
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
):
520 for spline
in self
.splines
:
521 rvLength
+= spline
.length
526 def RemoveShortSplines(self
, threshold
):
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
):
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])
552 def JoinGetFirstPair(self
, startEnd
, threshold
):
553 nrSplines
= len(self
.splines
)
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
]
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
):
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
]