1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 from . import mathematics
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
19 self
.handle_right
= handle_right
23 return BezierPoint(self
.handle_left
.copy(), self
.co
.copy(), self
.handle_right
.copy())
26 return BezierPoint(self
.handle_right
, self
.co
, self
.handle_left
)
29 tmp
= self
.handle_left
30 self
.handle_left
= self
.handle_right
31 self
.handle_right
= tmp
36 def FromBlenderBezierPoints(blenderBezierPoint1
, blenderBezierPoint2
):
37 bp1
= BezierPoint
.FromBlenderBezierPoint(blenderBezierPoint1
)
38 bp2
= BezierPoint
.FromBlenderBezierPoint(blenderBezierPoint2
)
40 return BezierSegment(bp1
, bp2
)
44 return BezierSegment(self
.bezierPoint1
.Copy(), self
.bezierPoint2
.Copy())
47 return BezierSegment(self
.bezierPoint2
.Reversed(), self
.bezierPoint1
.Reversed())
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
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
92 def CalcLength(self
, nrSamples
= 2):
93 nrSamplesFloat
= float(nrSamples
)
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
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
]
135 def FromSegments(listSegments
):
136 rvSpline
= BezierSpline(None)
138 rvSpline
.segments
= listSegments
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
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":
171 for seg
in self
.segments
: rvList
.append(seg
.bezierPoint1
)
172 if not self
.isCyclic
: rvList
.append(self
.segments
[-1].bezierPoint2
)
176 if attrName
== "resolutionPerSegment":
177 try: rvResPS
= int(self
.resolution
/ self
.nrSegments
)
179 if rvResPS
< 2: rvResPS
= 2
183 if attrName
== "length":
184 return self
.CalcLength()
189 def SetupSegments(self
):
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
))
199 bezierPoint1
= self
.bezierSpline
.bezier_points
[-1]
200 bezierPoint2
= self
.bezierSpline
.bezier_points
[0]
201 rvSegments
.append(BezierSegment
.FromBlenderBezierPoints(bezierPoint1
, bezierPoint2
))
206 def UpdateSegments(self
, newSegments
):
207 prevNrSegments
= len(self
.segments
)
208 diffNrSegments
= len(newSegments
) - prevNrSegments
209 if diffNrSegments
> 0:
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
]
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
230 print("### WARNING: UpdateSegments(): not diffNrSegments > 0")
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
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")
260 iSeg
= self
.segments
.index(segment
)
261 dPar
= 1.0 / self
.nrSegments
262 splinePar
= dPar
* (parameter
+ float(iSeg
))
264 res1
= int(splinePar
* self
.resolution
)
266 print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1
, "-- setting it to 2")
269 res2
= int((1.0 - splinePar
) * self
.resolution
)
271 print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2
, "-- setting it to 2")
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")
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
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")
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
)
369 if mode
== 'Insert_segment':
370 self
.JoinInsertSegment(spline2
)
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
]
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
431 for segment
in self
.segments
:
432 rvLength
+= segment
.CalcLength(nrSamples
= nrSamplesPerSegment
)
437 def GetLengthIsSmallerThan(self
, threshold
):
438 try: nrSamplesPerSegment
= int(self
.resolution
/ self
.nrSegments
)
439 except: nrSamplesPerSegment
= 2
440 if nrSamplesPerSegment
< 2: nrSamplesPerSegment
= 2
443 for segment
in self
.segments
:
444 length
+= segment
.CalcLength(nrSamples
= nrSamplesPerSegment
)
445 if not length
< threshold
: return False
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
474 def SetupSplines(self
):
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")
481 try: newSpline
= BezierSpline(spline
)
483 print("## EXCEPTION: newSpline = BezierSpline(spline)")
486 rvSplines
.append(newSpline
)
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
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
]
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
):
522 for spline
in self
.splines
:
523 rvLength
+= spline
.length
528 def RemoveShortSplines(self
, threshold
):
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
):
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])
554 def JoinGetFirstPair(self
, startEnd
, threshold
):
555 nrSplines
= len(self
.splines
)
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
]
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
):
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
]