chiark / gitweb /
Add back the ultimaker platform, and made the platform mesh simpler.
[cura.git] / Cura / slice / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / comb.py
1 """
2 This page is in the table of contents.
3 Comb is a craft plugin to bend the extruder travel paths around holes in the slices, to avoid stringers.
4
5 The comb manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb
7
8 ==Operation==
9 The default 'Activate Comb' checkbox is off.  When it is on, the functions described below will work, when it is off, nothing will be done.
10
11 ==Settings==
12 ===Running Jump Space===
13 Default: 2 mm
14
15 Defines the running jump space that is added before going from one island to another.  If the running jump space is greater than zero, the departure from the island will also be brought closer to the arrival point on the next island so that the stringer between islands will be shorter.  For an extruder with acceleration code, an extra space before leaving the island means that it will be going at high speed as it exits the island, which means the stringer between islands will be thinner.
16
17 ==Examples==
18 The following examples comb the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and comb.py.
19
20 > python comb.py
21 This brings up the comb dialog.
22
23 > python comb.py Screw Holder Bottom.stl
24 The comb tool is parsing the file:
25 Screw Holder Bottom.stl
26 ..
27 The comb tool has created the file:
28 .. Screw Holder Bottom_comb.gcode
29
30 """
31
32 from __future__ import absolute_import
33
34 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
35 from fabmetheus_utilities import archive
36 from fabmetheus_utilities import euclidean
37 from fabmetheus_utilities import gcodec
38 from fabmetheus_utilities import intercircle
39 from fabmetheus_utilities import settings
40 from skeinforge_application.skeinforge_utilities import skeinforge_craft
41 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
42 from skeinforge_application.skeinforge_utilities import skeinforge_profile
43 import sys
44
45
46 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
47 __date__ = '$Date: 2008/21/04 $'
48 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
49
50
51 def getCraftedText(fileName, text, repository=None):
52         "Comb a gcode linear move text."
53         return getCraftedTextFromText(archive.getTextIfEmpty(fileName, text), repository)
54
55 def getCraftedTextFromText(gcodeText, repository=None):
56         "Comb a gcode linear move text."
57         if gcodec.isProcedureDoneOrFileIsEmpty(gcodeText, 'comb'):
58                 return gcodeText
59         if repository == None:
60                 repository = settings.getReadRepository(CombRepository())
61         if not repository.activateComb.value:
62                 return gcodeText
63         return CombSkein().getCraftedGcode(gcodeText, repository)
64
65 def getJumpPoint(begin, end, loop, runningJumpSpace):
66         'Get running jump point inside loop.'
67         segment = begin - end
68         segmentLength = abs(segment)
69         if segmentLength == 0.0:
70                 return begin
71         segment /= segmentLength
72         distancePoint = DistancePoint(begin, loop, runningJumpSpace, segment)
73         if distancePoint.distance == runningJumpSpace:
74                 return distancePoint.point
75         effectiveDistance = distancePoint.distance
76         jumpPoint = distancePoint.point
77         segmentLeft = complex(0.70710678118654757, -0.70710678118654757)
78         distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentLeft)
79         distancePoint.distance *= 0.5
80         if distancePoint.distance > effectiveDistance:
81                 effectiveDistance = distancePoint.distance
82                 jumpPoint = distancePoint.point
83         segmentRight = complex(0.70710678118654757, 0.70710678118654757)
84         distancePoint = DistancePoint(begin, loop, runningJumpSpace, segmentRight)
85         distancePoint.distance *= 0.5
86         if distancePoint.distance > effectiveDistance:
87                 effectiveDistance = distancePoint.distance
88                 jumpPoint = distancePoint.point
89         return jumpPoint
90
91 def getJumpPointIfInside(boundary, otherPoint, edgeWidth, runningJumpSpace):
92         'Get the jump point if it is inside the boundary, otherwise return None.'
93         insetBoundary = intercircle.getSimplifiedInsetFromClockwiseLoop(boundary, -edgeWidth)
94         closestJumpDistanceIndex = euclidean.getClosestDistanceIndexToLine(otherPoint, insetBoundary)
95         jumpIndex = (closestJumpDistanceIndex.index + 1) % len(insetBoundary)
96         jumpPoint = euclidean.getClosestPointOnSegment(insetBoundary[closestJumpDistanceIndex.index], insetBoundary[jumpIndex], otherPoint)
97         jumpPoint = getJumpPoint(jumpPoint, otherPoint, boundary, runningJumpSpace)
98         if euclidean.isPointInsideLoop(boundary, jumpPoint):
99                 return jumpPoint
100         return None
101
102 def getNewRepository():
103         'Get new repository.'
104         return CombRepository()
105
106 def getPathsByIntersectedLoop(begin, end, loop):
107         'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
108         closestBeginDistanceIndex = euclidean.getClosestDistanceIndexToLine(begin, loop)
109         closestEndDistanceIndex = euclidean.getClosestDistanceIndexToLine(end, loop)
110         beginIndex = (closestBeginDistanceIndex.index + 1) % len(loop)
111         endIndex = (closestEndDistanceIndex.index + 1) % len(loop)
112         closestBegin = euclidean.getClosestPointOnSegment(loop[closestBeginDistanceIndex.index], loop[beginIndex], begin)
113         closestEnd = euclidean.getClosestPointOnSegment(loop[closestEndDistanceIndex.index], loop[endIndex], end)
114         clockwisePath = [closestBegin]
115         widdershinsPath = [closestBegin]
116         if closestBeginDistanceIndex.index != closestEndDistanceIndex.index:
117                 widdershinsPath += euclidean.getAroundLoop(beginIndex, endIndex, loop)
118                 clockwisePath += euclidean.getAroundLoop(endIndex, beginIndex, loop)[: : -1]
119         clockwisePath.append(closestEnd)
120         widdershinsPath.append(closestEnd)
121         return [clockwisePath, widdershinsPath]
122
123 def writeOutput(fileName, shouldAnalyze=True):
124         "Comb a gcode linear move file."
125         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'comb', shouldAnalyze)
126
127
128 class BoundarySegment(object):
129         'A boundary and segment.'
130         def __init__(self, begin):
131                 'Initialize'
132                 self.segment = [begin]
133
134         def getSegment(self, boundarySegmentIndex, boundarySegments, edgeWidth, runningJumpSpace):
135                 'Get both paths along the loop from the point closest to the begin to the point closest to the end.'
136                 nextBoundarySegment = boundarySegments[boundarySegmentIndex + 1]
137                 nextBegin = nextBoundarySegment.segment[0]
138                 end = getJumpPointIfInside(self.boundary, nextBegin, edgeWidth, runningJumpSpace)
139                 if end == None:
140                         end = self.segment[1]
141                 nextBegin = getJumpPointIfInside(nextBoundarySegment.boundary, end, edgeWidth, runningJumpSpace)
142                 if nextBegin != None:
143                         nextBoundarySegment.segment[0] = nextBegin
144                 return (self.segment[0], end)
145
146
147 class CombRepository(object):
148         "A class to handle the comb settings."
149         def __init__(self):
150                 "Set the default settings, execute title & settings fileName."
151                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.comb.html', self )
152                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Comb', self, '')
153                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Comb')
154                 self.activateComb = settings.BooleanSetting().getFromValue('Activate Comb', self, True )
155                 self.runningJumpSpace = settings.FloatSpin().getFromValue(0.0, 'Running Jump Space (mm):', self, 5.0, 2.0)
156                 self.executeTitle = 'Comb'
157
158         def execute(self):
159                 "Comb button has been clicked."
160                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
161                 for fileName in fileNames:
162                         writeOutput(fileName)
163
164
165 class CombSkein(object):
166         "A class to comb a skein of extrusions."
167         def __init__(self):
168                 'Initialize'
169                 self.boundaryLoop = None
170                 self.distanceFeedRate = gcodec.DistanceFeedRate()
171                 self.extruderActive = False
172                 self.layer = None
173                 self.layerCount = settings.LayerCount()
174                 self.layerTable = {}
175                 self.layerZ = None
176                 self.lineIndex = 0
177                 self.lines = None
178                 self.nextLayerZ = None
179                 self.oldLocation = None
180                 self.oldZ = None
181                 self.operatingFeedRatePerMinute = None
182                 self.travelFeedRateMinute = None
183                 self.widdershinTable = {}
184
185         def addGcodePathZ( self, feedRateMinute, path, z ):
186                 "Add a gcode path, without modifying the extruder, to the output."
187                 for point in path:
188                         self.distanceFeedRate.addGcodeMovementZWithFeedRate(feedRateMinute, point, z)
189
190         def addIfTravel(self, splitLine):
191                 "Add travel move around loops if the extruder is off."
192                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
193                 if not self.extruderActive and self.oldLocation != None:
194                         if len(self.getBoundaries()) > 0:
195                                 highestZ = max(location.z, self.oldLocation.z)
196                                 self.addGcodePathZ(self.travelFeedRateMinute, self.getAroundBetweenPath(self.oldLocation.dropAxis(), location.dropAxis()), highestZ)
197                 self.oldLocation = location
198
199         def addToLoop(self, location):
200                 "Add a location to loop."
201                 if self.layer == None:
202                         if not self.oldZ in self.layerTable:
203                                 self.layerTable[self.oldZ] = []
204                         self.layer = self.layerTable[self.oldZ]
205                 if self.boundaryLoop == None:
206                         self.boundaryLoop = []
207                         self.layer.append(self.boundaryLoop)
208                 self.boundaryLoop.append(location.dropAxis())
209
210         def getAroundBetweenLineSegment(self, begin, boundaries, end):
211                 'Get the path around the loops in the way of the original line segment.'
212                 aroundBetweenLineSegment = []
213                 boundaries = self.getBoundaries()
214                 points = []
215                 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
216                 boundaryIndexesIndex = 0
217                 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
218                         if boundaryIndexes[boundaryIndexesIndex + 1] == boundaryIndexes[boundaryIndexesIndex]:
219                                 loopFirst = boundaries[boundaryIndexes[boundaryIndexesIndex]]
220                                 pathBetween = self.getPathBetween(loopFirst, points[boundaryIndexesIndex : boundaryIndexesIndex + 4])
221                                 begin = points[boundaryIndexesIndex]
222                                 end = points[boundaryIndexesIndex + 3]
223                                 pathBetween = self.getInsidePointsAlong(begin, pathBetween[0], points) + pathBetween
224                                 pathBetween += self.getInsidePointsAlong(end, pathBetween[-1], points)
225                                 aroundBetweenLineSegment += pathBetween
226                                 boundaryIndexesIndex += 2
227                         else:
228                                 boundaryIndexesIndex += 1
229                 return aroundBetweenLineSegment
230
231         def getAroundBetweenPath(self, begin, end):
232                 'Get the path around the loops in the way of the original line segment.'
233                 aroundBetweenPath = []
234                 boundaries = self.getBoundaries()
235                 boundarySegments = self.getBoundarySegments(begin, boundaries, end)
236                 for boundarySegmentIndex, boundarySegment in enumerate(boundarySegments):
237                         segment = boundarySegment.segment
238                         if boundarySegmentIndex < len(boundarySegments) - 1 and self.runningJumpSpace > 0.0:
239                                 segment = boundarySegment.getSegment(boundarySegmentIndex, boundarySegments, self.edgeWidth, self.runningJumpSpace)
240                         aroundBetweenPath += self.getAroundBetweenLineSegment(segment[0], boundaries, segment[1])
241                         if boundarySegmentIndex < len(boundarySegments) - 1:
242                                 aroundBetweenPath.append(segment[1])
243                                 aroundBetweenPath.append(boundarySegments[boundarySegmentIndex + 1].segment[0])
244                 for pointIndex in xrange(len(aroundBetweenPath) - 1, -1, -1):
245                         pointBefore = begin
246                         beforeIndex = pointIndex - 1
247                         if beforeIndex >= 0:
248                                 pointBefore = aroundBetweenPath[beforeIndex]
249                         pointAfter = end
250                         afterIndex = pointIndex + 1
251                         if afterIndex < len(aroundBetweenPath):
252                                 pointAfter = aroundBetweenPath[afterIndex]
253                         if not euclidean.isLineIntersectingLoops(boundaries, pointBefore, pointAfter):
254                                 del aroundBetweenPath[pointIndex]
255                 return aroundBetweenPath
256
257         def getBoundaries(self):
258                 "Get boundaries for the layer."
259                 if self.layerZ in self.layerTable:
260                         return self.layerTable[self.layerZ]
261                 return []
262
263         def getBoundaryIndexes(self, begin, boundaries, end, points):
264                 'Get boundary indexes and set the points in the way of the original line segment.'
265                 boundaryIndexes = []
266                 points.append(begin)
267                 switchX = []
268                 segment = euclidean.getNormalized(end - begin)
269                 segmentYMirror = complex(segment.real, - segment.imag)
270                 beginRotated = segmentYMirror * begin
271                 endRotated = segmentYMirror * end
272                 y = beginRotated.imag
273                 for boundaryIndex in xrange(len(boundaries)):
274                         boundary = boundaries[boundaryIndex]
275                         boundaryRotated = euclidean.getRotatedComplexes(segmentYMirror, boundary)
276                         euclidean.addXIntersectionIndexesFromLoopY(boundaryRotated, boundaryIndex, switchX, y)
277                 switchX.sort()
278                 maximumX = max(beginRotated.real, endRotated.real)
279                 minimumX = min(beginRotated.real, endRotated.real)
280                 for xIntersection in switchX:
281                         if minimumX < xIntersection.x < maximumX:
282                                 point = segment * complex(xIntersection.x, y)
283                                 points.append(point)
284                                 boundaryIndexes.append(xIntersection.index)
285                 points.append(end)
286                 return boundaryIndexes
287
288         def getBoundarySegments(self, begin, boundaries, end):
289                 'Get the path broken into boundary segments whenever a different boundary is crossed.'
290                 boundarySegments = []
291                 boundarySegment = BoundarySegment(begin)
292                 boundarySegments.append(boundarySegment)
293                 points = []
294                 boundaryIndexes = self.getBoundaryIndexes(begin, boundaries, end, points)
295                 boundaryIndexesIndex = 0
296                 while boundaryIndexesIndex < len(boundaryIndexes) - 1:
297                         if boundaryIndexes[boundaryIndexesIndex + 1] != boundaryIndexes[boundaryIndexesIndex]:
298                                 boundarySegment.boundary = boundaries[boundaryIndexes[boundaryIndexesIndex]]
299                                 nextBoundary = boundaries[boundaryIndexes[boundaryIndexesIndex + 1]]
300                                 if euclidean.isWiddershins(boundarySegment.boundary) and euclidean.isWiddershins(nextBoundary):
301                                         boundarySegment.segment.append(points[boundaryIndexesIndex + 1])
302                                         boundarySegment = BoundarySegment(points[boundaryIndexesIndex + 2])
303                                         boundarySegment.boundary = nextBoundary
304                                         boundarySegments.append(boundarySegment)
305                                         boundaryIndexesIndex += 1
306                         boundaryIndexesIndex += 1
307                 boundarySegment.segment.append(points[-1])
308                 return boundarySegments
309
310         def getCraftedGcode(self, gcodeText, repository):
311                 "Parse gcode text and store the comb gcode."
312                 self.runningJumpSpace = repository.runningJumpSpace.value
313                 self.repository = repository
314                 self.lines = archive.getTextLines(gcodeText)
315                 self.parseInitialization()
316                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
317                         line = self.lines[lineIndex]
318                         self.parseBoundariesLayers(line)
319                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
320                         line = self.lines[lineIndex]
321                         self.parseLine(line)
322                 return self.distanceFeedRate.output.getvalue()
323
324         def getInsidePointsAlong(self, begin, end, points):
325                 'Get the points along the segment if it is required to keep the path inside the widdershin boundaries.'
326                 segment = end - begin
327                 segmentLength = abs(segment)
328                 if segmentLength < self.quadrupleEdgeWidth:
329                         return []
330                 segmentHalfPerimeter = self.halfEdgeWidth / segmentLength * segment
331                 justAfterBegin = begin + segmentHalfPerimeter
332                 justBeforeEnd = end - segmentHalfPerimeter
333                 widdershins = self.getWiddershins()
334                 if not euclidean.isLineIntersectingLoops(widdershins, justAfterBegin, justBeforeEnd):
335                         return []
336                 numberOfSteps = 10
337                 stepLength = (segmentLength - self.doubleEdgeWidth) / float(numberOfSteps)
338                 for step in xrange(1, numberOfSteps + 1):
339                         along = begin + stepLength * step
340                         if not euclidean.isLineIntersectingLoops(widdershins, along, justBeforeEnd):
341                                 return [along]
342                 return []
343
344         def getPathBetween(self, loop, points):
345                 "Add a path between the edge and the fill."
346                 paths = getPathsByIntersectedLoop(points[1], points[2], loop)
347                 shortestPath = paths[int(euclidean.getPathLength(paths[1]) < euclidean.getPathLength(paths[0]))]
348                 if len(shortestPath) < 2:
349                         return shortestPath
350                 if abs(points[1] - shortestPath[0]) > abs(points[1] - shortestPath[-1]):
351                         shortestPath.reverse()
352                 loopWiddershins = euclidean.isWiddershins(loop)
353                 pathBetween = []
354                 for pointIndex in xrange(len(shortestPath)):
355                         center = shortestPath[pointIndex]
356                         centerPerpendicular = None
357                         beginIndex = pointIndex - 1
358                         if beginIndex >= 0:
359                                 begin = shortestPath[beginIndex]
360                                 centerPerpendicular = intercircle.getWiddershinsByLength(center, begin, self.edgeWidth*2.0)
361                         centerEnd = None
362                         endIndex = pointIndex + 1
363                         if endIndex < len(shortestPath):
364                                 end = shortestPath[endIndex]
365                                 centerEnd = intercircle.getWiddershinsByLength(end, center, self.edgeWidth*2.0)
366                         if centerPerpendicular == None:
367                                 centerPerpendicular = centerEnd
368                         elif centerEnd != None:
369                                 centerPerpendicular = 0.5 * (centerPerpendicular + centerEnd)
370                         between = None
371                         if centerPerpendicular == None:
372                                 between = center
373                         if between == None:
374                                 centerSideWiddershins = center + centerPerpendicular
375                                 if euclidean.isPointInsideLoop(loop, centerSideWiddershins) == loopWiddershins:
376                                         between = centerSideWiddershins
377                         if between == None:
378                                 centerSideClockwise = center - centerPerpendicular
379                                 if euclidean.isPointInsideLoop(loop, centerSideClockwise) == loopWiddershins:
380                                         between = centerSideClockwise
381                         if between == None:
382                                 between = center
383                         pathBetween.append(between)
384                 return pathBetween
385
386         def getWiddershins(self):
387                 'Get widdershins for the layer.'
388                 if self.layerZ in self.widdershinTable:
389                         return self.widdershinTable[self.layerZ]
390                 self.widdershinTable[self.layerZ] = []
391                 for boundary in self.getBoundaries():
392                         if euclidean.isWiddershins(boundary):
393                                 self.widdershinTable[self.layerZ].append(boundary)
394                 return self.widdershinTable[self.layerZ]
395
396         def parseBoundariesLayers(self, line):
397                 "Parse a gcode line."
398                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
399                 if len(splitLine) < 1:
400                         return
401                 firstWord = splitLine[0]
402                 if firstWord == 'M103':
403                         self.boundaryLoop = None
404                 elif firstWord == '(<boundaryPoint>':
405                         location = gcodec.getLocationFromSplitLine(None, splitLine)
406                         self.addToLoop(location)
407                 elif firstWord == '(<layer>':
408                         self.boundaryLoop = None
409                         self.layer = None
410                         self.oldZ = float(splitLine[1])
411
412         def parseInitialization(self):
413                 'Parse gcode initialization and store the parameters.'
414                 for self.lineIndex in xrange(len(self.lines)):
415                         line = self.lines[self.lineIndex]
416                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
417                         firstWord = gcodec.getFirstWord(splitLine)
418                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
419                         if firstWord == '(</extruderInitialization>)':
420                                 self.distanceFeedRate.addTagBracketedProcedure('comb')
421                                 return
422                         elif firstWord == '(<edgeWidth>':
423                                 self.edgeWidth = float(splitLine[1])
424                                 self.doubleEdgeWidth = self.edgeWidth + self.edgeWidth
425                                 self.halfEdgeWidth = 0.5 * self.edgeWidth
426                                 self.quadrupleEdgeWidth = self.doubleEdgeWidth + self.doubleEdgeWidth
427                         elif firstWord == '(<travelFeedRatePerSecond>':
428                                 self.travelFeedRateMinute = 60.0 * float(splitLine[1])
429                         self.distanceFeedRate.addLine(line)
430
431         def parseLine(self, line):
432                 "Parse a gcode line and add it to the comb skein."
433                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
434                 if len(splitLine) < 1:
435                         return
436                 firstWord = splitLine[0]
437                 if self.distanceFeedRate.getIsAlteration(line):
438                         return
439                 if firstWord == 'G1':
440                         self.addIfTravel(splitLine)
441                         self.layerZ = self.nextLayerZ
442                 elif firstWord == 'M101':
443                         self.extruderActive = True
444                 elif firstWord == 'M103':
445                         self.extruderActive = False
446                 elif firstWord == '(<layer>':
447                         self.layerCount.printProgressIncrement('comb')
448                         self.nextLayerZ = float(splitLine[1])
449                         if self.layerZ == None:
450                                 self.layerZ = self.nextLayerZ
451                 self.distanceFeedRate.addLineCheckAlteration(line)
452
453
454 class DistancePoint(object):
455         'A class to get the distance of the point along a segment inside a loop.'
456         def __init__(self, begin, loop, runningJumpSpace, segment):
457                 'Initialize'
458                 self.distance = 0.0
459                 self.point = begin
460                 steps = 10
461                 spaceOverSteps = runningJumpSpace / float(steps)
462                 for numerator in xrange(1, steps + 1):
463                         distance = float(numerator) * spaceOverSteps
464                         point = begin + segment * distance
465                         if euclidean.isPointInsideLoop(loop, point):
466                                 self.distance = distance
467                                 self.point = point
468                         else:
469                                 return
470
471
472 def main():
473         "Display the comb dialog."
474         if len(sys.argv) > 1:
475                 writeOutput(' '.join(sys.argv[1 :]))
476         else:
477                 settings.startMainLoopFromConstructor(getNewRepository())
478
479 if __name__ == "__main__":
480         main()