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 / stretch.py
1 """
2 This page is in the table of contents.
3 Stretch is very important Skeinforge plugin that allows you to partially compensate for the fact that extruded holes are smaller then they should be.  It stretches the threads to partially compensate for filament shrinkage when extruded.
4
5 The stretch manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Stretch
7
8 Extruded holes are smaller than the model because while printing an arc the head is depositing filament on both sides of the arc but in the inside of the arc you actually need less material then on the outside of the arc. You can read more about this on the RepRap ArcCompensation page:
9 http://reprap.org/bin/view/Main/ArcCompensation
10
11 In general, stretch will widen holes and push corners out.  In practice the filament contraction will not be identical to the algorithm, so even once the optimal parameters are determined, the stretch script will not be able to eliminate the inaccuracies caused by contraction, but it should reduce them.
12
13 All the defaults assume that the thread sequence choice setting in fill is the edge being extruded first, then the loops, then the infill.  If the thread sequence choice is different, the optimal thread parameters will also be different.  In general, if the infill is extruded first, the infill would have to be stretched more so that even after the filament shrinkage, it would still be long enough to connect to the loop or edge.
14
15 Holes should be made with the correct area for their radius.  In other words, for example if your modeling program approximates a hole of radius one (area = pi) by making a square with the points at [(1,0), (0,1), (-1,0), (0,-1)] (area = 2), the radius should be increased by sqrt(pi/2).  This can be done in fabmetheus xml by writing:
16 radiusAreal='True'
17
18 in the attributes of the object or any parent of that object.  In other modeling programs, you'll have to this manually or make a script.  If area compensation is not done, then changing the stretch parameters to over compensate for too small hole areas will lead to incorrect compensation in other shapes.
19
20 ==Operation==
21 The default 'Activate Stretch' checkbox is off.  When it is on, the functions described below will work, when it is off, the functions will not be called.
22
23 ==Settings==
24 ===Loop Stretch Over Perimeter Width===
25 Default is 0.1.
26
27 Defines the ratio of the maximum amount the loop aka inner shell threads will be stretched compared to the edge width, in general this value should be the same as the 'Perimeter Outside Stretch Over Perimeter Width' setting.
28
29 ===Path Stretch Over Perimeter Width===
30 Default is zero.
31
32 Defines the ratio of the maximum amount the threads which are not loops, like the infill threads, will be stretched compared to the edge width.
33
34 ===Perimeter===
35 ====Perimeter Inside Stretch Over Perimeter Width====
36 Default is 0.32.
37
38 Defines the ratio of the maximum amount the inside edge thread will be stretched compared to the edge width, this is the most important setting in stretch.  The higher the value the more it will stretch the edge and the wider holes will be.  If the value is too small, the holes could be drilled out after fabrication, if the value is too high, the holes would be too wide and the part would have to junked.
39
40 ====Perimeter Outside Stretch Over Perimeter Width====
41 Default is 0.1.
42
43 Defines the ratio of the maximum amount the outside edge thread will be stretched compared to the edge width, in general this value should be around a third of the 'Perimeter Inside Stretch Over Perimeter Width' setting.
44
45 ===Stretch from Distance over Perimeter Width===
46 Default is two.
47
48 The stretch algorithm works by checking at each turning point on the extrusion path what the direction of the thread is at a distance of 'Stretch from Distance over Perimeter Width' times the edge width, on both sides, and moves the thread in the opposite direction.  So it takes the current turning-point, goes "Stretch from Distance over Perimeter Width" * "Perimeter Width" ahead, reads the direction at that point.  Then it goes the same distance in back in time, reads the direction at that other point.  It then moves the thread in the opposite direction, away from the center of the arc formed by these 2 points+directions.
49
50 The magnitude of the stretch increases with:
51 the amount that the direction of the two threads is similar and
52 by the '..Stretch Over Perimeter Width' ratio.
53
54 ==Examples==
55 The following examples stretch the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and stretch.py.
56
57 > python stretch.py
58 This brings up the stretch dialog.
59
60 > python stretch.py Screw Holder Bottom.stl
61 The stretch tool is parsing the file:
62 Screw Holder Bottom.stl
63 ..
64 The stretch tool has created the file:
65 .. Screw Holder Bottom_stretch.gcode
66
67 """
68
69 from __future__ import absolute_import
70
71 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
72 from fabmetheus_utilities import archive
73 from fabmetheus_utilities import euclidean
74 from fabmetheus_utilities import gcodec
75 from fabmetheus_utilities import settings
76 from skeinforge_application.skeinforge_utilities import skeinforge_craft
77 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
78 from skeinforge_application.skeinforge_utilities import skeinforge_profile
79 import sys
80
81
82 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
83 __date__ = '$Date: 2008/21/04 $'
84 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
85
86
87 #maybe speed up feedRate option
88 def getCraftedText( fileName, gcodeText, stretchRepository = None ):
89         "Stretch a gcode linear move text."
90         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), stretchRepository )
91
92 def getCraftedTextFromText( gcodeText, stretchRepository = None ):
93         "Stretch a gcode linear move text."
94         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'stretch'):
95                 return gcodeText
96         if stretchRepository == None:
97                 stretchRepository = settings.getReadRepository( StretchRepository() )
98         if not stretchRepository.activateStretch.value:
99                 return gcodeText
100         return StretchSkein().getCraftedGcode( gcodeText, stretchRepository )
101
102 def getNewRepository():
103         'Get new repository.'
104         return StretchRepository()
105
106 def writeOutput(fileName, shouldAnalyze=True):
107         "Stretch a gcode linear move file.  Chain stretch the gcode if it is not already stretched."
108         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'stretch', shouldAnalyze)
109
110
111 class LineIteratorBackward(object):
112         "Backward line iterator class."
113         def __init__( self, isLoop, lineIndex, lines ):
114                 self.firstLineIndex = None
115                 self.isLoop = isLoop
116                 self.lineIndex = lineIndex
117                 self.lines = lines
118
119         def getIndexBeforeNextDeactivate(self):
120                 "Get index two lines before the deactivate command."
121                 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
122                         line = self.lines[lineIndex]
123                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
124                         firstWord = gcodec.getFirstWord(splitLine)
125                         if firstWord == 'M103':
126                                 return lineIndex - 2
127                 print('This should never happen in stretch, no deactivate command was found for this thread.')
128                 raise StopIteration, "You've reached the end of the line."
129
130         def getNext(self):
131                 "Get next line going backward or raise exception."
132                 while self.lineIndex > 3:
133                         if self.lineIndex == self.firstLineIndex:
134                                 raise StopIteration, "You've reached the end of the line."
135                         if self.firstLineIndex == None:
136                                 self.firstLineIndex = self.lineIndex
137                         nextLineIndex = self.lineIndex - 1
138                         line = self.lines[self.lineIndex]
139                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
140                         firstWord = gcodec.getFirstWord(splitLine)
141                         if firstWord == 'M103':
142                                 if self.isLoop:
143                                         nextLineIndex = self.getIndexBeforeNextDeactivate()
144                                 else:
145                                         raise StopIteration, "You've reached the end of the line."
146                         if firstWord == 'G1':
147                                 if self.isBeforeExtrusion():
148                                         if self.isLoop:
149                                                 nextLineIndex = self.getIndexBeforeNextDeactivate()
150                                         else:
151                                                 raise StopIteration, "You've reached the end of the line."
152                                 else:
153                                         self.lineIndex = nextLineIndex
154                                         return line
155                         self.lineIndex = nextLineIndex
156                 raise StopIteration, "You've reached the end of the line."
157
158         def isBeforeExtrusion(self):
159                 "Determine if index is two or more before activate command."
160                 linearMoves = 0
161                 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
162                         line = self.lines[lineIndex]
163                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
164                         firstWord = gcodec.getFirstWord(splitLine)
165                         if firstWord == 'G1':
166                                 linearMoves += 1
167                         if firstWord == 'M101':
168                                 return linearMoves > 0
169                         if firstWord == 'M103':
170                                 return False
171                 print('This should never happen in isBeforeExtrusion in stretch, no activate command was found for this thread.')
172                 return False
173
174
175 class LineIteratorForward(object):
176         "Forward line iterator class."
177         def __init__( self, isLoop, lineIndex, lines ):
178                 self.firstLineIndex = None
179                 self.isLoop = isLoop
180                 self.lineIndex = lineIndex
181                 self.lines = lines
182
183         def getIndexJustAfterActivate(self):
184                 "Get index just after the activate command."
185                 for lineIndex in xrange( self.lineIndex - 1, 3, - 1 ):
186                         line = self.lines[lineIndex]
187                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
188                         firstWord = gcodec.getFirstWord(splitLine)
189                         if firstWord == 'M101':
190                                 return lineIndex + 1
191                 print('This should never happen in stretch, no activate command was found for this thread.')
192                 raise StopIteration, "You've reached the end of the line."
193
194         def getNext(self):
195                 "Get next line or raise exception."
196                 while self.lineIndex < len(self.lines):
197                         if self.lineIndex == self.firstLineIndex:
198                                 raise StopIteration, "You've reached the end of the line."
199                         if self.firstLineIndex == None:
200                                 self.firstLineIndex = self.lineIndex
201                         nextLineIndex = self.lineIndex + 1
202                         line = self.lines[self.lineIndex]
203                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
204                         firstWord = gcodec.getFirstWord(splitLine)
205                         if firstWord == 'M103':
206                                 if self.isLoop:
207                                         nextLineIndex = self.getIndexJustAfterActivate()
208                                 else:
209                                         raise StopIteration, "You've reached the end of the line."
210                         self.lineIndex = nextLineIndex
211                         if firstWord == 'G1':
212                                 return line
213                 raise StopIteration, "You've reached the end of the line."
214
215
216 class StretchRepository(object):
217         "A class to handle the stretch settings."
218         def __init__(self):
219                 "Set the default settings, execute title & settings fileName."
220                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.stretch.html', self )
221                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Stretch', self, '')
222                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Stretch')
223                 self.activateStretch = settings.BooleanSetting().getFromValue('Activate Stretch', self, False )
224                 self.crossLimitDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 3.0, 'Cross Limit Distance Over Perimeter Width (ratio):', self, 10.0, 5.0 )
225                 self.loopStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.05, 'Loop Stretch Over Perimeter Width (ratio):', self, 0.25, 0.11 )
226                 self.pathStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.0, 'Path Stretch Over Perimeter Width (ratio):', self, 0.2, 0.0 )
227                 settings.LabelSeparator().getFromRepository(self)
228                 settings.LabelDisplay().getFromName('- Perimeter -', self )
229                 self.edgeInsideStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.12, 'Perimeter Inside Stretch Over Perimeter Width (ratio):', self, 0.52, 0.32 )
230                 self.edgeOutsideStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.05, 'Perimeter Outside Stretch Over Perimeter Width (ratio):', self, 0.25, 0.1 )
231                 settings.LabelSeparator().getFromRepository(self)
232                 self.stretchFromDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 1.0, 'Stretch From Distance Over Perimeter Width (ratio):', self, 3.0, 2.0 )
233                 self.executeTitle = 'Stretch'
234
235         def execute(self):
236                 "Stretch button has been clicked."
237                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
238                 for fileName in fileNames:
239                         writeOutput(fileName)
240
241
242 class StretchSkein(object):
243         "A class to stretch a skein of extrusions."
244         def __init__(self):
245                 self.distanceFeedRate = gcodec.DistanceFeedRate()
246                 self.edgeWidth = 0.4
247                 self.extruderActive = False
248                 self.feedRateMinute = 959.0
249                 self.isLoop = False
250                 self.layerCount = settings.LayerCount()
251                 self.lineIndex = 0
252                 self.lines = None
253                 self.oldLocation = None
254
255         def getCraftedGcode( self, gcodeText, stretchRepository ):
256                 "Parse gcode text and store the stretch gcode."
257                 self.lines = archive.getTextLines(gcodeText)
258                 self.stretchRepository = stretchRepository
259                 self.parseInitialization()
260                 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
261                         line = self.lines[self.lineIndex]
262                         self.parseStretch(line)
263                 return self.distanceFeedRate.output.getvalue()
264
265         def getCrossLimitedStretch( self, crossLimitedStretch, crossLineIterator, locationComplex ):
266                 "Get cross limited relative stretch for a location."
267                 try:
268                         line = crossLineIterator.getNext()
269                 except StopIteration:
270                         return crossLimitedStretch
271                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
272                 pointComplex = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine).dropAxis()
273                 pointMinusLocation = locationComplex - pointComplex
274                 pointMinusLocationLength = abs( pointMinusLocation )
275                 if pointMinusLocationLength <= self.crossLimitDistanceFraction:
276                         return crossLimitedStretch
277                 parallelNormal = pointMinusLocation / pointMinusLocationLength
278                 parallelStretch = euclidean.getDotProduct( parallelNormal, crossLimitedStretch ) * parallelNormal
279                 if pointMinusLocationLength > self.crossLimitDistance:
280                         return parallelStretch
281                 crossNormal = complex( parallelNormal.imag, - parallelNormal.real )
282                 crossStretch = euclidean.getDotProduct( crossNormal, crossLimitedStretch ) * crossNormal
283                 crossPortion = ( self.crossLimitDistance - pointMinusLocationLength ) / self.crossLimitDistanceRemainder
284                 return parallelStretch + crossStretch * crossPortion
285
286         def getRelativeStretch( self, locationComplex, lineIterator ):
287                 "Get relative stretch for a location."
288                 lastLocationComplex = locationComplex
289                 oldTotalLength = 0.0
290                 pointComplex = locationComplex
291                 totalLength = 0.0
292                 while 1:
293                         try:
294                                 line = lineIterator.getNext()
295                         except StopIteration:
296                                 locationMinusPoint = locationComplex - pointComplex
297                                 locationMinusPointLength = abs( locationMinusPoint )
298                                 if locationMinusPointLength > 0.0:
299                                         return locationMinusPoint / locationMinusPointLength
300                                 return complex()
301                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
302                         firstWord = splitLine[0]
303                         pointComplex = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine).dropAxis()
304                         locationMinusPoint = lastLocationComplex - pointComplex
305                         locationMinusPointLength = abs( locationMinusPoint )
306                         totalLength += locationMinusPointLength
307                         if totalLength >= self.stretchFromDistance:
308                                 distanceFromRatio = ( self.stretchFromDistance - oldTotalLength ) / locationMinusPointLength
309                                 totalPoint = distanceFromRatio * pointComplex + ( 1.0 - distanceFromRatio ) * lastLocationComplex
310                                 locationMinusTotalPoint = locationComplex - totalPoint
311                                 return locationMinusTotalPoint / self.stretchFromDistance
312                         lastLocationComplex = pointComplex
313                         oldTotalLength = totalLength
314
315         def getStretchedLine( self, splitLine ):
316                 "Get stretched gcode line."
317                 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
318                 self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
319                 self.oldLocation = location
320                 if self.extruderActive and self.threadMaximumAbsoluteStretch > 0.0:
321                         return self.getStretchedLineFromIndexLocation( self.lineIndex - 1, self.lineIndex + 1, location )
322                 if self.isJustBeforeExtrusion() and self.threadMaximumAbsoluteStretch > 0.0:
323                         return self.getStretchedLineFromIndexLocation( self.lineIndex - 1, self.lineIndex + 1, location )
324                 return self.lines[self.lineIndex]
325
326         def getStretchedLineFromIndexLocation( self, indexPreviousStart, indexNextStart, location ):
327                 "Get stretched gcode line from line index and location."
328                 crossIteratorForward = LineIteratorForward( self.isLoop, indexNextStart, self.lines )
329                 crossIteratorBackward = LineIteratorBackward( self.isLoop, indexPreviousStart, self.lines )
330                 iteratorForward = LineIteratorForward( self.isLoop, indexNextStart, self.lines )
331                 iteratorBackward = LineIteratorBackward( self.isLoop, indexPreviousStart, self.lines )
332                 locationComplex = location.dropAxis()
333                 relativeStretch = self.getRelativeStretch( locationComplex, iteratorForward ) + self.getRelativeStretch( locationComplex, iteratorBackward )
334                 relativeStretch *= 0.8
335                 relativeStretch = self.getCrossLimitedStretch( relativeStretch, crossIteratorForward, locationComplex )
336                 relativeStretch = self.getCrossLimitedStretch( relativeStretch, crossIteratorBackward, locationComplex )
337                 relativeStretchLength = abs( relativeStretch )
338                 if relativeStretchLength > 1.0:
339                         relativeStretch /= relativeStretchLength
340                 absoluteStretch = relativeStretch * self.threadMaximumAbsoluteStretch
341                 stretchedPoint = location.dropAxis() + absoluteStretch
342                 return self.distanceFeedRate.getLinearGcodeMovementWithFeedRate( self.feedRateMinute, stretchedPoint, location.z )
343
344         def isJustBeforeExtrusion(self):
345                 "Determine if activate command is before linear move command."
346                 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
347                         line = self.lines[lineIndex]
348                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
349                         firstWord = gcodec.getFirstWord(splitLine)
350                         if firstWord == 'G1' or firstWord == 'M103':
351                                 return False
352                         if firstWord == 'M101':
353                                 return True
354 #               print('This should never happen in isJustBeforeExtrusion in stretch, no activate or deactivate command was found for this thread.')
355                 return False
356
357         def parseInitialization(self):
358                 'Parse gcode initialization and store the parameters.'
359                 for self.lineIndex in xrange(len(self.lines)):
360                         line = self.lines[self.lineIndex]
361                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
362                         firstWord = gcodec.getFirstWord(splitLine)
363                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
364                         if firstWord == '(</extruderInitialization>)':
365                                 self.distanceFeedRate.addTagBracketedProcedure('stretch')
366                                 return
367                         elif firstWord == '(<edgeWidth>':
368                                 edgeWidth = float(splitLine[1])
369                                 self.crossLimitDistance = self.edgeWidth * self.stretchRepository.crossLimitDistanceOverEdgeWidth.value
370                                 self.loopMaximumAbsoluteStretch = self.edgeWidth * self.stretchRepository.loopStretchOverEdgeWidth.value
371                                 self.pathAbsoluteStretch = self.edgeWidth * self.stretchRepository.pathStretchOverEdgeWidth.value
372                                 self.edgeInsideAbsoluteStretch = self.edgeWidth * self.stretchRepository.edgeInsideStretchOverEdgeWidth.value
373                                 self.edgeOutsideAbsoluteStretch = self.edgeWidth * self.stretchRepository.edgeOutsideStretchOverEdgeWidth.value
374                                 self.stretchFromDistance = self.stretchRepository.stretchFromDistanceOverEdgeWidth.value * edgeWidth
375                                 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
376                                 self.crossLimitDistanceFraction = 0.333333333 * self.crossLimitDistance
377                                 self.crossLimitDistanceRemainder = self.crossLimitDistance - self.crossLimitDistanceFraction
378                         self.distanceFeedRate.addLine(line)
379
380         def parseStretch(self, line):
381                 "Parse a gcode line and add it to the stretch skein."
382                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
383                 if len(splitLine) < 1:
384                         return
385                 firstWord = splitLine[0]
386                 if firstWord == 'G1':
387                         line = self.getStretchedLine(splitLine)
388                 elif firstWord == 'M101':
389                         self.extruderActive = True
390                 elif firstWord == 'M103':
391                         self.extruderActive = False
392                         self.setStretchToPath()
393                 elif firstWord == '(<layer>':
394                         self.layerCount.printProgressIncrement('stretch')
395                 elif firstWord == '(<loop>':
396                         self.isLoop = True
397                         self.threadMaximumAbsoluteStretch = self.loopMaximumAbsoluteStretch
398                 elif firstWord == '(</loop>)':
399                         self.setStretchToPath()
400                 elif firstWord == '(<edge>':
401                         self.isLoop = True
402                         self.threadMaximumAbsoluteStretch = self.edgeInsideAbsoluteStretch
403                         if splitLine[1] == 'outer':
404                                 self.threadMaximumAbsoluteStretch = self.edgeOutsideAbsoluteStretch
405                 elif firstWord == '(</edge>)':
406                         self.setStretchToPath()
407                 self.distanceFeedRate.addLine(line)
408
409         def setStretchToPath(self):
410                 "Set the thread stretch to path stretch and is loop false."
411                 self.isLoop = False
412                 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
413
414
415 def main():
416         "Display the stretch dialog."
417         if len(sys.argv) > 1:
418                 writeOutput(' '.join(sys.argv[1 :]))
419         else:
420                 settings.startMainLoopFromConstructor(getNewRepository())
421
422 if __name__ == "__main__":
423         main()