1 """
2 Intercircle is a collection of utilities for intersecting circles, used to get smooth loops around a collection of points and inset & outset loops.
3
4 """
5
6 from fabmetheus_utilities.vector3 import Vector3
7 from fabmetheus_utilities import euclidean
8 import math
9
10 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
11 __date__ = '$Date: 2008/21/04 $'
12 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
13
14
15 globalDecreasingRadiusMultipliers = [1.0, 0.55, 0.35, 0.2]
16
17
19 'Add a circle intersection loop.'
20 firstCircleIntersection = circleIntersectionLoop[0]
21 circleIntersectionAhead = firstCircleIntersection
22 for circleIntersectionIndex in xrange(len(circleIntersections) + 1):
23 circleIntersectionAhead = circleIntersectionAhead.getCircleIntersectionAhead()
24 if circleIntersectionAhead == firstCircleIntersection or circleIntersectionAhead == None:
25 firstCircleIntersection.steppedOn = True
26 return
27 circleIntersectionAhead.addToList(circleIntersectionLoop)
28 firstCircleIntersection.steppedOn = True
29 print( 'addCircleIntersectionLoop would have gone into an endless loop, this should never happen.' )
30 print( 'circleIntersectionLoop' )
31 for circleIntersection in circleIntersectionLoop:
32 print(circleIntersection)
33 print(circleIntersection.circleNodeAhead)
34 print(circleIntersection.circleNodeBehind)
35 print('firstCircleIntersection')
36 print(firstCircleIntersection)
37 print('circleIntersections')
38 for circleIntersection in circleIntersections:
39 print(circleIntersection)
40
42 'Get circular end cap.'
43 beginMinusEnd = begin - end
44 beginMinusEndLength = abs(beginMinusEnd)
45 if beginMinusEndLength <= 0.0:
46 points.append(begin)
47 return
48 beginMinusEnd *= radius / beginMinusEndLength
49 perpendicular = complex(-beginMinusEnd.imag, beginMinusEnd.real)
50 numberOfSides = 20
51 numberOfPositiveSides = numberOfSides / 2
52 totalAngle = 0.0
53 angle = euclidean.globalTau / float(numberOfSides)
54
55 dotProductMultiplier = 2.0 - 1.0 / math.cos(0.5 * angle)
56 for sideIndex in xrange(numberOfPositiveSides + 1):
57 circumferentialPoint = math.sin(totalAngle) * beginMinusEnd + math.cos(totalAngle) * perpendicular
58 points.append(begin + circumferentialPoint * dotProductMultiplier)
59 totalAngle += angle
60
61 -def addHalfPath(path, points, radius, thresholdRatio=0.9):
62 'Add the points from every point on a half path and between points.'
63 lessThanRadius = 0.7853 * radius
64 for pointIndex in xrange(len(path) - 1):
65 begin = path[pointIndex]
66 center = path[pointIndex + 1]
67 centerBegin = getWiddershinsByLength(begin, center, radius)
68 if centerBegin != None:
69 addPointsFromSegment(begin + centerBegin, center + centerBegin, points, lessThanRadius, thresholdRatio)
70 endIndex = pointIndex + 2
71 if endIndex < len(path):
72 end = path[endIndex]
73 centerEnd = getWiddershinsByLength(center, end, radius)
74 if centerBegin != None and centerEnd != None:
75 centerPerpendicular = 0.5 * (centerBegin + centerEnd)
76 points.append(center + centerPerpendicular)
77 if euclidean.getCrossProduct(centerBegin, centerEnd) < 0.0:
78 points.append(center + centerBegin)
79 points.append(center + centerEnd)
80 else:
81 points.append(center)
82 addEndCap(path[0], path[1], points, radius)
83
85 'Get inset point with possible intersection from clockwise triple, out from widdershins loop.'
86 centerMinusBegin = center - begin
87 centerMinusBeginLength = abs(centerMinusBegin)
88 centerMinusBeginClockwise = None
89 if centerMinusBeginLength > 0.0:
90 centerMinusBeginClockwise = complex(centerMinusBegin.imag, -centerMinusBegin.real) / centerMinusBeginLength
91 endMinusCenter = end - center
92 endMinusCenterLength = abs(endMinusCenter)
93 endMinusCenterClockwise = None
94 if endMinusCenterLength > 0.0:
95 endMinusCenterClockwise = complex(endMinusCenter.imag, -endMinusCenter.real) / endMinusCenterLength
96 if centerMinusBeginClockwise == None and endMinusCenterClockwise == None:
97 return
98 if centerMinusBeginClockwise == None:
99 loop.append(center + endMinusCenterClockwise * radius)
100 return
101 if endMinusCenterClockwise == None:
102 loop.append(center + centerMinusBeginClockwise * radius)
103 return
104 centerClockwise = 0.5 * (centerMinusBeginClockwise + endMinusCenterClockwise)
105 dotProduct = euclidean.getDotProduct(centerMinusBeginClockwise, centerClockwise)
106 loop.append(center + centerClockwise * radius / max(0.4, abs(dotProduct)))
107
108 -def addOrbits( distanceFeedRate, loop, orbitalFeedRatePerSecond, temperatureChangeTime, z ):
109 'Add orbits with the extruder off.'
110 timeInOrbit = 0.0
111 while timeInOrbit < temperatureChangeTime:
112 for point in loop:
113 distanceFeedRate.addGcodeMovementZWithFeedRate( 60.0 * orbitalFeedRatePerSecond, point, z )
114 timeInOrbit += euclidean.getLoopLength(loop) / orbitalFeedRatePerSecond
115
116 -def addOrbitsIfLarge( distanceFeedRate, loop, orbitalFeedRatePerSecond, temperatureChangeTime, z ):
117 'Add orbits with the extruder off if the orbits are large enough.'
118 if orbitsAreLarge( loop, temperatureChangeTime ):
119 addOrbits( distanceFeedRate, loop, orbitalFeedRatePerSecond, temperatureChangeTime, z )
120
122 'Add point complexes between the endpoints of a segment.'
123 if radius <= 0.0:
124 print('This should never happen, radius should never be zero or less in addPointsFromSegment in intercircle.')
125 thresholdRadius = radius * thresholdRatio
126 thresholdDiameter = thresholdRadius + thresholdRadius
127 segment = pointEnd - pointBegin
128 segmentLength = abs(segment)
129 extraCircles = int( math.floor( segmentLength / thresholdDiameter ) )
130 if extraCircles < 1:
131 return
132 if segmentLength == 0.0:
133 print('This should never happen, segmentLength = 0.0 in intercircle.')
134 print('pointBegin')
135 print( pointBegin )
136 print( pointEnd )
137 return
138 if extraCircles < 2:
139 lengthIncrement = segmentLength / ( float(extraCircles) + 1.0 )
140 segment *= lengthIncrement / segmentLength
141 pointBegin += segment
142 else:
143 pointBegin += segment * thresholdDiameter / segmentLength
144 remainingLength = segmentLength - thresholdDiameter - thresholdDiameter
145 lengthIncrement = remainingLength / ( float(extraCircles) - 1.0 )
146 segment *= lengthIncrement / segmentLength
147 for circleIndex in xrange(extraCircles):
148 points.append( pointBegin )
149 pointBegin += segment
150
155
160
165
169
173
179
188
196
207
209 'Get the complex centers of the circle intersection loops from circle nodes.'
210 if len( circleNodes ) < 2:
211 return []
212 circleIntersections = getCircleIntersectionsFromCircleNodes( circleNodes )
213 circleIntersectionLoops = getCircleIntersectionLoops( circleIntersections )
214 centers = []
215 for circleIntersectionLoop in circleIntersectionLoops:
216 loop = []
217 for circleIntersection in circleIntersectionLoop:
218 loop.append(circleIntersection.circleNodeAhead.actualPoint)
219 centers.append( loop )
220 return centers
221
226
231
236
238 'Get all the circle intersections which exist between all the circle nodes.'
239 if len( circleNodes ) < 1:
240 return []
241 circleIntersections = []
242 index = 0
243 pixelTable = {}
244 for circleNode in circleNodes:
245 euclidean.addElementToPixelListFromPoint(circleNode, pixelTable, circleNode.dividedPoint)
246 accumulatedCircleNodeTable = {}
247 for circleNodeIndex in xrange(len(circleNodes)):
248 circleNodeBehind = circleNodes[circleNodeIndex]
249 circleNodeIndexMinusOne = circleNodeIndex - 1
250 if circleNodeIndexMinusOne >= 0:
251 circleNodeAdditional = circleNodes[circleNodeIndexMinusOne]
252 euclidean.addElementToPixelListFromPoint(circleNodeAdditional, accumulatedCircleNodeTable, 0.5 * circleNodeAdditional.dividedPoint)
253 withinNodes = circleNodeBehind.getWithinNodes(accumulatedCircleNodeTable)
254 for circleNodeAhead in withinNodes:
255 circleIntersectionForward = CircleIntersection(circleNodeAhead, index, circleNodeBehind)
256 if not circleIntersectionForward.isWithinCircles(pixelTable):
257 circleIntersections.append(circleIntersectionForward)
258 circleNodeBehind.circleIntersections.append(circleIntersectionForward)
259 index += 1
260 circleIntersectionBackward = CircleIntersection(circleNodeBehind, index, circleNodeAhead)
261 if not circleIntersectionBackward.isWithinCircles(pixelTable):
262 circleIntersections.append(circleIntersectionBackward)
263 circleNodeAhead.circleIntersections.append(circleIntersectionBackward)
264 index += 1
265 return circleIntersections
266
268 'Get all the loops going through the circle intersections.'
269 circleIntersectionLoops = []
270 for circleIntersection in circleIntersections:
271 if not circleIntersection.steppedOn:
272 circleIntersectionLoop = [ circleIntersection ]
273 circleIntersectionLoops.append( circleIntersectionLoop )
274 addCircleIntersectionLoop( circleIntersectionLoop, circleIntersections )
275 return circleIntersectionLoops
276
278 'Get the circle nodes from every point on a loop and between points.'
279 radius = abs(radius)
280 points = getPointsFromLoop( loop, radius, thresholdRatio )
281 return getCircleNodesFromPoints( points, radius )
282
284 'Get the circle nodes from a path.'
285 circleNodes = []
286 oneOverRadius = 1.000001 / radius
287 points = euclidean.getAwayPoints(points, radius)
288 for point in points:
289 circleNodes.append(CircleNode(oneOverRadius, point))
290 return circleNodes
291
306
308 'Get the inset loops, which might overlap.'
309 insetLoops = []
310 for loop in loops:
311 insetLoops += getInsetLoopsFromLoop(loop, inset)
312 return insetLoops
313
321
335
356
366
371
373 'Get the largest inset loop from the loop, even if the radius has to be shrunk and even if there is still no inset loop.'
374 global globalDecreasingRadiusMultipliers
375 for decreasingRadiusMultiplier in globalDecreasingRadiusMultipliers:
376 decreasingRadius = radius * decreasingRadiusMultiplier
377 largestInsetLoop = getLargestInsetLoopFromLoop( loop, decreasingRadius )
378 if len( largestInsetLoop ) > 0:
379 return largestInsetLoop
380 print('This should never happen, there should always be a largestInsetLoop in getLargestInsetLoopFromLoopRegardless in intercircle.')
381 print(loop)
382 return loop
383
385 'Get the loops going round in a given direction.'
386 directionalLoops = []
387 for loop in loops:
388 if euclidean.isWiddershins(loop) == isWiddershins:
389 directionalLoops.append(loop)
390 return directionalLoops
391
393 'Get the points from every point on a loop and between points.'
394 radius = abs(radius)
395 points = []
396 for pointIndex in xrange(len(loop)):
397 pointBegin = loop[pointIndex]
398 pointEnd = loop[(pointIndex + 1) % len(loop)]
399 points.append( pointBegin )
400 addPointsFromSegment( pointBegin, pointEnd, points, radius, thresholdRatio )
401 return points
402
404 'Get the points from every point on a loop and between points.'
405 points = []
406 for loop in loops:
407 points += getPointsFromLoop(loop, radius, thresholdRatio)
408 return points
409
411 'Get the points from every point on a path and between points.'
412 if len(path) < 1:
413 return []
414 if len(path) < 2:
415 return path
416 radius = abs(radius)
417 points = []
418 addHalfPath(path, points, radius, thresholdRatio)
419 addHalfPath(path[: : -1], points, radius, thresholdRatio)
420 return points
421
430
432 'Get the widdershins by length.'
433 endMinusBegin = end - begin
434 endMinusBeginLength = abs(endMinusBegin)
435 if endMinusBeginLength <= 0.0:
436 return None
437 endMinusBegin *= length / endMinusBeginLength
438 return complex(-endMinusBegin.imag, endMinusBegin.real)
439
441 'Get loop without intersections.'
442 lastLoopLength = len( loop )
443 while lastLoopLength > 3:
444 removeIntersection( loop )
445 if len( loop ) == lastLoopLength:
446 return loop
447 lastLoopLength = len( loop )
448 return loop
449
455
457 'Determine if the a loop is intersecting another loop.'
458 for pointIndex in xrange(len(loop)):
459 pointFirst = loop[pointIndex]
460 pointSecond = loop[(pointIndex + 1) % len(loop)]
461 segment = pointFirst - pointSecond
462 normalizedSegment = euclidean.getNormalized(segment)
463 segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
464 segmentFirstPoint = segmentYMirror * pointFirst
465 segmentSecondPoint = segmentYMirror * pointSecond
466 if euclidean.isLoopIntersectingInsideXSegment( anotherLoop, segmentFirstPoint.real, segmentSecondPoint.real, segmentYMirror, segmentFirstPoint.imag ):
467 return True
468 return False
469
471 'Determine if the orbits are large enough.'
472 if len(loop) < 1:
473 print('Zero length loop which was skipped over, this should never happen.')
474 return False
475 return temperatureChangeTime > 1.5
476
478 'Get loop without the first intersection.'
479 for pointIndex, ahead in enumerate(loop):
480 behind = loop[ ( pointIndex + len( loop ) - 1 ) % len( loop ) ]
481 behindEnd = loop[ ( pointIndex + len( loop ) - 2 ) % len( loop ) ]
482 behindMidpoint = 0.5 * ( behind + behindEnd )
483 aheadEnd = loop[ (pointIndex + 1) % len( loop ) ]
484 aheadMidpoint = 0.5 * ( ahead + aheadEnd )
485 normalizedSegment = behind - behindMidpoint
486 normalizedSegmentLength = abs( normalizedSegment )
487 if normalizedSegmentLength > 0.0:
488 normalizedSegment /= normalizedSegmentLength
489 segmentYMirror = complex(normalizedSegment.real, -normalizedSegment.imag)
490 behindRotated = segmentYMirror * behind
491 behindMidpointRotated = segmentYMirror * behindMidpoint
492 aheadRotated = segmentYMirror * ahead
493 aheadMidpointRotated = segmentYMirror * aheadMidpoint
494 y = behindRotated.imag
495 xIntersection = euclidean.getXIntersectionIfExists( aheadRotated, aheadMidpointRotated, y )
496 if xIntersection != None:
497 if xIntersection > min( behindMidpointRotated.real, behindRotated.real ) and xIntersection < max( behindMidpointRotated.real, behindRotated.real ):
498 intersectionPoint = normalizedSegment * complex( xIntersection, y )
499 loop[ ( pointIndex + len( loop ) - 1 ) % len( loop ) ] = intersectionPoint
500 del loop[pointIndex]
501 return
502
503
505 'A class to hold a bounding loop composed of a minimum complex, a maximum complex and an outset loop.'
507 'Determine whether this bounding loop is identical to other one.'
508 if other == None:
509 return False
510 return self.minimum == other.minimum and self.maximum == other.maximum and self.loop == other.loop
511
513 'Get the string representation of this bounding loop.'
514 return '%s, %s, %s' % ( self.minimum, self.maximum, self.loop )
515
522
524 'Outset the bounding rectangle and loop by a distance.'
525 outsetBoundingLoop = BoundingLoop()
526 outsetBoundingLoop.maximum = self.maximum + complex( outsetDistance, outsetDistance )
527 outsetBoundingLoop.minimum = self.minimum - complex( outsetDistance, outsetDistance )
528 greaterThanOutsetDistance = 1.1 * outsetDistance
529 centers = getCentersFromLoopDirection( True, self.loop, greaterThanOutsetDistance )
530 outsetBoundingLoop.loop = getSimplifiedInsetFromClockwiseLoop( centers[0], outsetDistance )
531 return outsetBoundingLoop
532
534 'Determine if this bounding loop is entirely inside another bounding loop.'
535 if self.minimum.imag < anotherBoundingLoop.minimum.imag or self.minimum.real < anotherBoundingLoop.minimum.real:
536 return False
537 if self.maximum.imag > anotherBoundingLoop.maximum.imag or self.maximum.real > anotherBoundingLoop.maximum.real:
538 return False
539 for point in self.loop:
540 if euclidean.getNumberOfIntersectionsToLeft( anotherBoundingLoop.loop, point ) % 2 == 0:
541 return False
542 return not isLoopIntersectingLoop( anotherBoundingLoop.loop, self.loop )
543
555
557 'Determine if this bounding loop is intersecting another bounding loop in a list.'
558 for boundingLoop in boundingLoops:
559 if self.isOverlappingAnother( boundingLoop ):
560 return True
561 return False
562
564 'Determine if the rectangle of this bounding loop is missing the rectangle of another bounding loop.'
565 if self.maximum.imag < anotherBoundingLoop.minimum.imag or self.maximum.real < anotherBoundingLoop.minimum.real:
566 return True
567 return self.minimum.imag > anotherBoundingLoop.maximum.imag or self.minimum.real > anotherBoundingLoop.maximum.real
568
569
571 'A class to hold a center and an outset.'
573 'Set the center and outset.'
574 self.center = center
575 self.outset = outset
576
578 'Get the string representation of this CenterOutset.'
579 return '%s\n%s' % (self.center, self.outset)
580
581
583 'An intersection of two complex circles.'
584 - def __init__( self, circleNodeAhead, index, circleNodeBehind ):
585 self.aheadMinusBehind = 0.5 * ( circleNodeAhead.dividedPoint - circleNodeBehind.dividedPoint )
586 self.circleNodeAhead = circleNodeAhead
587 self.circleNodeBehind = circleNodeBehind
588 self.index = index
589 self.steppedOn = False
590 demichordWidth = math.sqrt( 1.0 - self.aheadMinusBehind.real * self.aheadMinusBehind.real - self.aheadMinusBehind.imag * self.aheadMinusBehind.imag )
591 rotatedClockwiseQuarter = complex( self.aheadMinusBehind.imag, - self.aheadMinusBehind.real )
592 rotatedClockwiseQuarterLength = abs( rotatedClockwiseQuarter )
593 if rotatedClockwiseQuarterLength == 0:
594 print('this should never happen, rotatedClockwiseQuarter in getDemichord in intercircle is 0')
595 print( circleNodeAhead.dividedPoint )
596 print( circleNodeBehind.dividedPoint )
597 self.demichord = 0.0
598 else:
599 self.demichord = rotatedClockwiseQuarter * demichordWidth / rotatedClockwiseQuarterLength
600 self.positionRelativeToBehind = self.aheadMinusBehind + self.demichord
601
603 'Get the string representation of this CircleIntersection.'
604 return '%s, %s, %s, %s' % (self.index, self.getAbsolutePosition(), self.circleNodeBehind, self.circleNodeAhead)
605
606 - def addToList( self, circleIntersectionPath ):
607 'Add this to the circle intersection path, setting stepped on to be true.'
608 self.steppedOn = True
609 circleIntersectionPath.append(self)
610
612 'Get the absolute position.'
613 return self.positionRelativeToBehind + self.circleNodeBehind.dividedPoint
614
616 'Get the first circle intersection on the circle node ahead.'
617 circleIntersections = self.circleNodeAhead.circleIntersections
618 circleIntersectionAhead = None
619 largestDot = -912345678.0
620 for circleIntersection in circleIntersections:
621 if not circleIntersection.steppedOn:
622 circleIntersectionRelativeToMidpoint = euclidean.getNormalized(circleIntersection.positionRelativeToBehind + self.aheadMinusBehind)
623 dot = euclidean.getDotProduct(self.demichord, circleIntersectionRelativeToMidpoint)
624 if dot > largestDot:
625 largestDot = dot
626 circleIntersectionAhead = circleIntersection
627 if circleIntersectionAhead == None:
628 print('Warning, circleIntersectionAhead in getCircleIntersectionAhead in intercircle is None for:')
629 print(self.circleNodeAhead.dividedPoint)
630 print('circleIntersectionsAhead')
631 for circleIntersection in circleIntersections:
632 print(circleIntersection.circleNodeAhead.dividedPoint)
633 print('circleIntersectionsBehind')
634 for circleIntersection in self.circleNodeBehind.circleIntersections:
635 print(circleIntersection.circleNodeAhead.dividedPoint)
636 print('This may lead to a loop not being sliced.')
637 print('If this is a problem, you may as well send a bug report, even though I probably can not fix this particular problem.')
638 return circleIntersectionAhead
639
641 'Determine if this circle intersection is within the circle node circles.'
642 absolutePosition = self.getAbsolutePosition()
643 squareValues = euclidean.getSquareValuesFromPoint(pixelTable, absolutePosition)
644 for squareValue in squareValues:
645 if abs(squareValue.dividedPoint - absolutePosition) < 1.0:
646 if squareValue != self.circleNodeAhead and squareValue != self.circleNodeBehind:
647 return True
648 return False
649
650
652 'A complex node of complex circle intersections.'
653 - def __init__(self, oneOverRadius, point):
654 self.actualPoint = point
655 self.circleIntersections = []
656 self.dividedPoint = point * oneOverRadius
657
658
660 'Get the string representation of this CircleNode.'
661
662 return '%s, %s' % (self.dividedPoint, len(self.circleIntersections))
663
665 'Get the nodes this circle node is within.'
666 withinNodes = []
667 squareValues = euclidean.getSquareValuesFromPoint(pixelTable, 0.5 * self.dividedPoint)
668 for squareValue in squareValues:
669 if abs(self.dividedPoint - squareValue.dividedPoint) < 2.0:
670 withinNodes.append(squareValue)
671 return withinNodes
672