1from . import mathematics 2 3import bpy 4 5 6class BezierPoint: 7 @staticmethod 8 def FromBlenderBezierPoint(blenderBezierPoint): 9 return BezierPoint(blenderBezierPoint.handle_left, blenderBezierPoint.co, blenderBezierPoint.handle_right) 10 11 12 def __init__(self, handle_left, co, handle_right): 13 self.handle_left = handle_left 14 self.co = co 15 self.handle_right = handle_right 16 17 18 def Copy(self): 19 return BezierPoint(self.handle_left.copy(), self.co.copy(), self.handle_right.copy()) 20 21 def Reversed(self): 22 return BezierPoint(self.handle_right, self.co, self.handle_left) 23 24 def Reverse(self): 25 tmp = self.handle_left 26 self.handle_left = self.handle_right 27 self.handle_right = tmp 28 29 30class BezierSegment: 31 @staticmethod 32 def FromBlenderBezierPoints(blenderBezierPoint1, blenderBezierPoint2): 33 bp1 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint1) 34 bp2 = BezierPoint.FromBlenderBezierPoint(blenderBezierPoint2) 35 36 return BezierSegment(bp1, bp2) 37 38 39 def Copy(self): 40 return BezierSegment(self.bezierPoint1.Copy(), self.bezierPoint2.Copy()) 41 42 def Reversed(self): 43 return BezierSegment(self.bezierPoint2.Reversed(), self.bezierPoint1.Reversed()) 44 45 def Reverse(self): 46 # make a copy, otherwise neighboring segment may be affected 47 tmp = self.bezierPoint1.Copy() 48 self.bezierPoint1 = self.bezierPoint2.Copy() 49 self.bezierPoint2 = tmp 50 self.bezierPoint1.Reverse() 51 self.bezierPoint2.Reverse() 52 53 54 def __init__(self, bezierPoint1, bezierPoint2): 55 # bpy.types.BezierSplinePoint 56 # ## NOTE/TIP: copy() helps with repeated (intersection) action -- ?? 57 self.bezierPoint1 = bezierPoint1.Copy() 58 self.bezierPoint2 = bezierPoint2.Copy() 59 60 self.ctrlPnt0 = self.bezierPoint1.co 61 self.ctrlPnt1 = self.bezierPoint1.handle_right 62 self.ctrlPnt2 = self.bezierPoint2.handle_left 63 self.ctrlPnt3 = self.bezierPoint2.co 64 65 self.coeff0 = self.ctrlPnt0 66 self.coeff1 = self.ctrlPnt0 * (-3.0) + self.ctrlPnt1 * (+3.0) 67 self.coeff2 = self.ctrlPnt0 * (+3.0) + self.ctrlPnt1 * (-6.0) + self.ctrlPnt2 * (+3.0) 68 self.coeff3 = self.ctrlPnt0 * (-1.0) + self.ctrlPnt1 * (+3.0) + self.ctrlPnt2 * (-3.0) + self.ctrlPnt3 69 70 71 def CalcPoint(self, parameter = 0.5): 72 parameter2 = parameter * parameter 73 parameter3 = parameter * parameter2 74 75 rvPoint = self.coeff0 + self.coeff1 * parameter + self.coeff2 * parameter2 + self.coeff3 * parameter3 76 77 return rvPoint 78 79 80 def CalcDerivative(self, parameter = 0.5): 81 parameter2 = parameter * parameter 82 83 rvPoint = self.coeff1 + self.coeff2 * parameter * 2.0 + self.coeff3 * parameter2 * 3.0 84 85 return rvPoint 86 87 88 def CalcLength(self, nrSamples = 2): 89 nrSamplesFloat = float(nrSamples) 90 rvLength = 0.0 91 for iSample in range(nrSamples): 92 par1 = float(iSample) / nrSamplesFloat 93 par2 = float(iSample + 1) / nrSamplesFloat 94 95 point1 = self.CalcPoint(parameter = par1) 96 point2 = self.CalcPoint(parameter = par2) 97 diff12 = point1 - point2 98 99 rvLength += diff12.magnitude 100 101 return rvLength 102 103 104 #http://en.wikipedia.org/wiki/De_Casteljau's_algorithm 105 def CalcSplitPoint(self, parameter = 0.5): 106 par1min = 1.0 - parameter 107 108 bez00 = self.ctrlPnt0 109 bez01 = self.ctrlPnt1 110 bez02 = self.ctrlPnt2 111 bez03 = self.ctrlPnt3 112 113 bez10 = bez00 * par1min + bez01 * parameter 114 bez11 = bez01 * par1min + bez02 * parameter 115 bez12 = bez02 * par1min + bez03 * parameter 116 117 bez20 = bez10 * par1min + bez11 * parameter 118 bez21 = bez11 * par1min + bez12 * parameter 119 120 bez30 = bez20 * par1min + bez21 * parameter 121 122 bezPoint1 = BezierPoint(self.bezierPoint1.handle_left, bez00, bez10) 123 bezPointNew = BezierPoint(bez20, bez30, bez21) 124 bezPoint2 = BezierPoint(bez12, bez03, self.bezierPoint2.handle_right) 125 126 return [bezPoint1, bezPointNew, bezPoint2] 127 128 129class BezierSpline: 130 @staticmethod 131 def FromSegments(listSegments): 132 rvSpline = BezierSpline(None) 133 134 rvSpline.segments = listSegments 135 136 return rvSpline 137 138 139 def __init__(self, blenderBezierSpline): 140 if not blenderBezierSpline is None: 141 if blenderBezierSpline.type != 'BEZIER': 142 print("## ERROR:", "blenderBezierSpline.type != 'BEZIER'") 143 raise Exception("blenderBezierSpline.type != 'BEZIER'") 144 if len(blenderBezierSpline.bezier_points) < 1: 145 if not blenderBezierSpline.use_cyclic_u: 146 print("## ERROR:", "len(blenderBezierSpline.bezier_points) < 1") 147 raise Exception("len(blenderBezierSpline.bezier_points) < 1") 148 149 self.bezierSpline = blenderBezierSpline 150 151 self.resolution = 12 152 self.isCyclic = False 153 if not self.bezierSpline is None: 154 self.resolution = self.bezierSpline.resolution_u 155 self.isCyclic = self.bezierSpline.use_cyclic_u 156 157 self.segments = self.SetupSegments() 158 159 160 def __getattr__(self, attrName): 161 if attrName == "nrSegments": 162 return len(self.segments) 163 164 if attrName == "bezierPoints": 165 rvList = [] 166 167 for seg in self.segments: rvList.append(seg.bezierPoint1) 168 if not self.isCyclic: rvList.append(self.segments[-1].bezierPoint2) 169 170 return rvList 171 172 if attrName == "resolutionPerSegment": 173 try: rvResPS = int(self.resolution / self.nrSegments) 174 except: rvResPS = 2 175 if rvResPS < 2: rvResPS = 2 176 177 return rvResPS 178 179 if attrName == "length": 180 return self.CalcLength() 181 182 return None 183 184 185 def SetupSegments(self): 186 rvSegments = [] 187 if self.bezierSpline is None: return rvSegments 188 189 nrBezierPoints = len(self.bezierSpline.bezier_points) 190 for iBezierPoint in range(nrBezierPoints - 1): 191 bezierPoint1 = self.bezierSpline.bezier_points[iBezierPoint] 192 bezierPoint2 = self.bezierSpline.bezier_points[iBezierPoint + 1] 193 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2)) 194 if self.isCyclic: 195 bezierPoint1 = self.bezierSpline.bezier_points[-1] 196 bezierPoint2 = self.bezierSpline.bezier_points[0] 197 rvSegments.append(BezierSegment.FromBlenderBezierPoints(bezierPoint1, bezierPoint2)) 198 199 return rvSegments 200 201 202 def UpdateSegments(self, newSegments): 203 prevNrSegments = len(self.segments) 204 diffNrSegments = len(newSegments) - prevNrSegments 205 if diffNrSegments > 0: 206 newBezierPoints = [] 207 for segment in newSegments: newBezierPoints.append(segment.bezierPoint1) 208 if not self.isCyclic: newBezierPoints.append(newSegments[-1].bezierPoint2) 209 210 self.bezierSpline.bezier_points.add(diffNrSegments) 211 212 for i, bezPoint in enumerate(newBezierPoints): 213 blBezPoint = self.bezierSpline.bezier_points[i] 214 215 blBezPoint.tilt = 0 216 blBezPoint.radius = 1.0 217 218 blBezPoint.handle_left_type = 'FREE' 219 blBezPoint.handle_left = bezPoint.handle_left 220 blBezPoint.co = bezPoint.co 221 blBezPoint.handle_right_type = 'FREE' 222 blBezPoint.handle_right = bezPoint.handle_right 223 224 self.segments = newSegments 225 else: 226 print("### WARNING: UpdateSegments(): not diffNrSegments > 0") 227 228 229 def Reversed(self): 230 revSegments = [] 231 232 for iSeg in reversed(range(self.nrSegments)): revSegments.append(self.segments[iSeg].Reversed()) 233 234 rvSpline = BezierSpline.FromSegments(revSegments) 235 rvSpline.resolution = self.resolution 236 rvSpline.isCyclic = self.isCyclic 237 238 return rvSpline 239 240 241 def Reverse(self): 242 revSegments = [] 243 244 for iSeg in reversed(range(self.nrSegments)): 245 self.segments[iSeg].Reverse() 246 revSegments.append(self.segments[iSeg]) 247 248 self.segments = revSegments 249 250 251 def CalcDivideResolution(self, segment, parameter): 252 if not segment in self.segments: 253 print("### WARNING: InsertPoint(): not segment in self.segments") 254 return None 255 256 iSeg = self.segments.index(segment) 257 dPar = 1.0 / self.nrSegments 258 splinePar = dPar * (parameter + float(iSeg)) 259 260 res1 = int(splinePar * self.resolution) 261 if res1 < 2: 262 print("### WARNING: CalcDivideResolution(): res1 < 2 -- res1: %d" % res1, "-- setting it to 2") 263 res1 = 2 264 265 res2 = int((1.0 - splinePar) * self.resolution) 266 if res2 < 2: 267 print("### WARNING: CalcDivideResolution(): res2 < 2 -- res2: %d" % res2, "-- setting it to 2") 268 res2 = 2 269 270 return [res1, res2] 271 # return [self.resolution, self.resolution] 272 273 274 def CalcPoint(self, parameter): 275 nrSegs = self.nrSegments 276 277 segmentIndex = int(nrSegs * parameter) 278 if segmentIndex < 0: segmentIndex = 0 279 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1 280 281 segmentParameter = nrSegs * parameter - segmentIndex 282 if segmentParameter < 0.0: segmentParameter = 0.0 283 if segmentParameter > 1.0: segmentParameter = 1.0 284 285 return self.segments[segmentIndex].CalcPoint(parameter = segmentParameter) 286 287 288 def CalcDerivative(self, parameter): 289 nrSegs = self.nrSegments 290 291 segmentIndex = int(nrSegs * parameter) 292 if segmentIndex < 0: segmentIndex = 0 293 if segmentIndex > (nrSegs - 1): segmentIndex = nrSegs - 1 294 295 segmentParameter = nrSegs * parameter - segmentIndex 296 if segmentParameter < 0.0: segmentParameter = 0.0 297 if segmentParameter > 1.0: segmentParameter = 1.0 298 299 return self.segments[segmentIndex].CalcDerivative(parameter = segmentParameter) 300 301 302 def InsertPoint(self, segment, parameter): 303 if not segment in self.segments: 304 print("### WARNING: InsertPoint(): not segment in self.segments") 305 return 306 iSeg = self.segments.index(segment) 307 nrSegments = len(self.segments) 308 309 splitPoints = segment.CalcSplitPoint(parameter = parameter) 310 bezPoint1 = splitPoints[0] 311 bezPointNew = splitPoints[1] 312 bezPoint2 = splitPoints[2] 313 314 segment.bezierPoint1.handle_right = bezPoint1.handle_right 315 segment.bezierPoint2 = bezPointNew 316 317 if iSeg < (nrSegments - 1): 318 nextSeg = self.segments[iSeg + 1] 319 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left 320 else: 321 if self.isCyclic: 322 nextSeg = self.segments[0] 323 nextSeg.bezierPoint1.handle_left = bezPoint2.handle_left 324 325 326 newSeg = BezierSegment(bezPointNew, bezPoint2) 327 self.segments.insert(iSeg + 1, newSeg) 328 329 330 def Split(self, segment, parameter): 331 if not segment in self.segments: 332 print("### WARNING: InsertPoint(): not segment in self.segments") 333 return None 334 iSeg = self.segments.index(segment) 335 nrSegments = len(self.segments) 336 337 splitPoints = segment.CalcSplitPoint(parameter = parameter) 338 bezPoint1 = splitPoints[0] 339 bezPointNew = splitPoints[1] 340 bezPoint2 = splitPoints[2] 341 342 343 newSpline1Segments = [] 344 for iSeg1 in range(iSeg): newSpline1Segments.append(self.segments[iSeg1]) 345 if len(newSpline1Segments) > 0: newSpline1Segments[-1].bezierPoint2.handle_right = bezPoint1.handle_right 346 newSpline1Segments.append(BezierSegment(bezPoint1, bezPointNew)) 347 348 newSpline2Segments = [] 349 newSpline2Segments.append(BezierSegment(bezPointNew, bezPoint2)) 350 for iSeg2 in range(iSeg + 1, nrSegments): newSpline2Segments.append(self.segments[iSeg2]) 351 if len(newSpline2Segments) > 1: newSpline2Segments[1].bezierPoint1.handle_left = newSpline2Segments[0].bezierPoint2.handle_left 352 353 354 newSpline1 = BezierSpline.FromSegments(newSpline1Segments) 355 newSpline2 = BezierSpline.FromSegments(newSpline2Segments) 356 357 return [newSpline1, newSpline2] 358 359 360 def Join(self, spline2, mode = 'At_midpoint'): 361 if mode == 'At_midpoint': 362 self.JoinAtMidpoint(spline2) 363 return 364 365 if mode == 'Insert_segment': 366 self.JoinInsertSegment(spline2) 367 return 368 369 print("### ERROR: Join(): unknown mode:", mode) 370 371 372 def JoinAtMidpoint(self, spline2): 373 bezPoint1 = self.segments[-1].bezierPoint2 374 bezPoint2 = spline2.segments[0].bezierPoint1 375 376 mpHandleLeft = bezPoint1.handle_left.copy() 377 mpCo = (bezPoint1.co + bezPoint2.co) * 0.5 378 mpHandleRight = bezPoint2.handle_right.copy() 379 mpBezPoint = BezierPoint(mpHandleLeft, mpCo, mpHandleRight) 380 381 self.segments[-1].bezierPoint2 = mpBezPoint 382 spline2.segments[0].bezierPoint1 = mpBezPoint 383 for seg2 in spline2.segments: self.segments.append(seg2) 384 385 self.resolution += spline2.resolution 386 self.isCyclic = False # is this ok? 387 388 389 def JoinInsertSegment(self, spline2): 390 self.segments.append(BezierSegment(self.segments[-1].bezierPoint2, spline2.segments[0].bezierPoint1)) 391 for seg2 in spline2.segments: self.segments.append(seg2) 392 393 self.resolution += spline2.resolution # extra segment will usually be short -- impact on resolution negligable 394 395 self.isCyclic = False # is this ok? 396 397 398 def RefreshInScene(self): 399 bezierPoints = self.bezierPoints 400 401 currNrBezierPoints = len(self.bezierSpline.bezier_points) 402 diffNrBezierPoints = len(bezierPoints) - currNrBezierPoints 403 if diffNrBezierPoints > 0: self.bezierSpline.bezier_points.add(diffNrBezierPoints) 404 405 for i, bezPoint in enumerate(bezierPoints): 406 blBezPoint = self.bezierSpline.bezier_points[i] 407 408 blBezPoint.tilt = 0 409 blBezPoint.radius = 1.0 410 411 blBezPoint.handle_left_type = 'FREE' 412 blBezPoint.handle_left = bezPoint.handle_left 413 blBezPoint.co = bezPoint.co 414 blBezPoint.handle_right_type = 'FREE' 415 blBezPoint.handle_right = bezPoint.handle_right 416 417 self.bezierSpline.use_cyclic_u = self.isCyclic 418 self.bezierSpline.resolution_u = self.resolution 419 420 421 def CalcLength(self): 422 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments) 423 except: nrSamplesPerSegment = 2 424 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2 425 426 rvLength = 0.0 427 for segment in self.segments: 428 rvLength += segment.CalcLength(nrSamples = nrSamplesPerSegment) 429 430 return rvLength 431 432 433 def GetLengthIsSmallerThan(self, threshold): 434 try: nrSamplesPerSegment = int(self.resolution / self.nrSegments) 435 except: nrSamplesPerSegment = 2 436 if nrSamplesPerSegment < 2: nrSamplesPerSegment = 2 437 438 length = 0.0 439 for segment in self.segments: 440 length += segment.CalcLength(nrSamples = nrSamplesPerSegment) 441 if not length < threshold: return False 442 443 return True 444 445 446class Curve: 447 def __init__(self, blenderCurve): 448 self.curve = blenderCurve 449 self.curveData = blenderCurve.data 450 451 self.splines = self.SetupSplines() 452 453 454 def __getattr__(self, attrName): 455 if attrName == "nrSplines": 456 return len(self.splines) 457 458 if attrName == "length": 459 return self.CalcLength() 460 461 if attrName == "worldMatrix": 462 return self.curve.matrix_world 463 464 if attrName == "location": 465 return self.curve.location 466 467 return None 468 469 470 def SetupSplines(self): 471 rvSplines = [] 472 for spline in self.curveData.splines: 473 if spline.type != 'BEZIER': 474 print("## WARNING: only bezier splines are supported, atm; other types are ignored") 475 continue 476 477 try: newSpline = BezierSpline(spline) 478 except: 479 print("## EXCEPTION: newSpline = BezierSpline(spline)") 480 continue 481 482 rvSplines.append(newSpline) 483 484 return rvSplines 485 486 487 def RebuildInScene(self): 488 self.curveData.splines.clear() 489 490 for spline in self.splines: 491 blSpline = self.curveData.splines.new('BEZIER') 492 blSpline.use_cyclic_u = spline.isCyclic 493 blSpline.resolution_u = spline.resolution 494 495 bezierPoints = [] 496 for segment in spline.segments: bezierPoints.append(segment.bezierPoint1) 497 if not spline.isCyclic: bezierPoints.append(spline.segments[-1].bezierPoint2) 498 #else: print("????", "spline.isCyclic") 499 500 nrBezierPoints = len(bezierPoints) 501 blSpline.bezier_points.add(nrBezierPoints - 1) 502 503 for i, blBezPoint in enumerate(blSpline.bezier_points): 504 bezPoint = bezierPoints[i] 505 506 blBezPoint.tilt = 0 507 blBezPoint.radius = 1.0 508 509 blBezPoint.handle_left_type = 'FREE' 510 blBezPoint.handle_left = bezPoint.handle_left 511 blBezPoint.co = bezPoint.co 512 blBezPoint.handle_right_type = 'FREE' 513 blBezPoint.handle_right = bezPoint.handle_right 514 515 516 def CalcLength(self): 517 rvLength = 0.0 518 for spline in self.splines: 519 rvLength += spline.length 520 521 return rvLength 522 523 524 def RemoveShortSplines(self, threshold): 525 splinesToRemove = [] 526 527 for spline in self.splines: 528 if spline.GetLengthIsSmallerThan(threshold): splinesToRemove.append(spline) 529 530 for spline in splinesToRemove: self.splines.remove(spline) 531 532 return len(splinesToRemove) 533 534 535 def JoinNeighbouringSplines(self, startEnd, threshold, mode): 536 nrJoins = 0 537 538 while True: 539 firstPair = self.JoinGetFirstPair(startEnd, threshold) 540 if firstPair is None: break 541 542 firstPair[0].Join(firstPair[1], mode) 543 self.splines.remove(firstPair[1]) 544 545 nrJoins += 1 546 547 return nrJoins 548 549 550 def JoinGetFirstPair(self, startEnd, threshold): 551 nrSplines = len(self.splines) 552 553 if startEnd: 554 for iCurrentSpline in range(nrSplines): 555 currentSpline = self.splines[iCurrentSpline] 556 557 for iNextSpline in range(iCurrentSpline + 1, nrSplines): 558 nextSpline = self.splines[iNextSpline] 559 560 currEndPoint = currentSpline.segments[-1].bezierPoint2.co 561 nextStartPoint = nextSpline.segments[0].bezierPoint1.co 562 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline] 563 564 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co 565 currStartPoint = currentSpline.segments[0].bezierPoint1.co 566 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline] 567 568 return None 569 else: 570 for iCurrentSpline in range(nrSplines): 571 currentSpline = self.splines[iCurrentSpline] 572 573 for iNextSpline in range(iCurrentSpline + 1, nrSplines): 574 nextSpline = self.splines[iNextSpline] 575 576 currEndPoint = currentSpline.segments[-1].bezierPoint2.co 577 nextStartPoint = nextSpline.segments[0].bezierPoint1.co 578 if mathematics.IsSamePoint(currEndPoint, nextStartPoint, threshold): return [currentSpline, nextSpline] 579 580 nextEndPoint = nextSpline.segments[-1].bezierPoint2.co 581 currStartPoint = currentSpline.segments[0].bezierPoint1.co 582 if mathematics.IsSamePoint(nextEndPoint, currStartPoint, threshold): return [nextSpline, currentSpline] 583 584 if mathematics.IsSamePoint(currEndPoint, nextEndPoint, threshold): 585 nextSpline.Reverse() 586 #print("## ", "nextSpline.Reverse()") 587 return [currentSpline, nextSpline] 588 589 if mathematics.IsSamePoint(currStartPoint, nextStartPoint, threshold): 590 currentSpline.Reverse() 591 #print("## ", "currentSpline.Reverse()") 592 return [currentSpline, nextSpline] 593 594 return None 595