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.
5 The stretch manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Stretch
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
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.
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.
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:
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.
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.
24 ===Loop Stretch Over Perimeter Width===
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.
29 ===Path Stretch Over Perimeter Width===
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.
35 ====Perimeter Inside Stretch Over Perimeter Width====
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.
40 ====Perimeter Outside Stretch Over Perimeter Width====
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.
45 ===Stretch from Distance over Perimeter Width===
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.
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.
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.
58 This brings up the stretch dialog.
60 > python stretch.py Screw Holder Bottom.stl
61 The stretch tool is parsing the file:
62 Screw Holder Bottom.stl
64 The stretch tool has created the file:
65 .. Screw Holder Bottom_stretch.gcode
69 from __future__ import absolute_import
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
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'
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 )
92 def getCraftedTextFromText( gcodeText, stretchRepository = None ):
93 "Stretch a gcode linear move text."
94 if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'stretch'):
96 if stretchRepository == None:
97 stretchRepository = settings.getReadRepository( StretchRepository() )
98 if not stretchRepository.activateStretch.value:
100 return StretchSkein().getCraftedGcode( gcodeText, stretchRepository )
102 def getNewRepository():
103 'Get new repository.'
104 return StretchRepository()
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)
111 class LineIteratorBackward(object):
112 "Backward line iterator class."
113 def __init__( self, isLoop, lineIndex, lines ):
114 self.firstLineIndex = None
116 self.lineIndex = lineIndex
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':
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."
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':
143 nextLineIndex = self.getIndexBeforeNextDeactivate()
145 raise StopIteration, "You've reached the end of the line."
146 if firstWord == 'G1':
147 if self.isBeforeExtrusion():
149 nextLineIndex = self.getIndexBeforeNextDeactivate()
151 raise StopIteration, "You've reached the end of the line."
153 self.lineIndex = nextLineIndex
155 self.lineIndex = nextLineIndex
156 raise StopIteration, "You've reached the end of the line."
158 def isBeforeExtrusion(self):
159 "Determine if index is two or more before activate command."
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':
167 if firstWord == 'M101':
168 return linearMoves > 0
169 if firstWord == 'M103':
171 print('This should never happen in isBeforeExtrusion in stretch, no activate command was found for this thread.')
175 class LineIteratorForward(object):
176 "Forward line iterator class."
177 def __init__( self, isLoop, lineIndex, lines ):
178 self.firstLineIndex = None
180 self.lineIndex = lineIndex
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':
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."
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':
207 nextLineIndex = self.getIndexJustAfterActivate()
209 raise StopIteration, "You've reached the end of the line."
210 self.lineIndex = nextLineIndex
211 if firstWord == 'G1':
213 raise StopIteration, "You've reached the end of the line."
216 class StretchRepository(object):
217 "A class to handle the stretch settings."
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'
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)
242 class StretchSkein(object):
243 "A class to stretch a skein of extrusions."
245 self.distanceFeedRate = gcodec.DistanceFeedRate()
247 self.extruderActive = False
248 self.feedRateMinute = 959.0
250 self.layerCount = settings.LayerCount()
253 self.oldLocation = None
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()
265 def getCrossLimitedStretch( self, crossLimitedStretch, crossLineIterator, locationComplex ):
266 "Get cross limited relative stretch for a location."
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
286 def getRelativeStretch( self, locationComplex, lineIterator ):
287 "Get relative stretch for a location."
288 lastLocationComplex = locationComplex
290 pointComplex = locationComplex
294 line = lineIterator.getNext()
295 except StopIteration:
296 locationMinusPoint = locationComplex - pointComplex
297 locationMinusPointLength = abs( locationMinusPoint )
298 if locationMinusPointLength > 0.0:
299 return locationMinusPoint / locationMinusPointLength
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
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]
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 )
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':
352 if firstWord == 'M101':
354 # print('This should never happen in isJustBeforeExtrusion in stretch, no activate or deactivate command was found for this thread.')
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')
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)
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:
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>':
397 self.threadMaximumAbsoluteStretch = self.loopMaximumAbsoluteStretch
398 elif firstWord == '(</loop>)':
399 self.setStretchToPath()
400 elif firstWord == '(<edge>':
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)
409 def setStretchToPath(self):
410 "Set the thread stretch to path stretch and is loop false."
412 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
416 "Display the stretch dialog."
417 if len(sys.argv) > 1:
418 writeOutput(' '.join(sys.argv[1 :]))
420 settings.startMainLoopFromConstructor(getNewRepository())
422 if __name__ == "__main__":