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
70 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
73 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
74 from fabmetheus_utilities.vector3 import Vector3
75 from fabmetheus_utilities import archive
76 from fabmetheus_utilities import euclidean
77 from fabmetheus_utilities import gcodec
78 from fabmetheus_utilities import intercircle
79 from fabmetheus_utilities import settings
80 from skeinforge_application.skeinforge_utilities import skeinforge_craft
81 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
82 from skeinforge_application.skeinforge_utilities import skeinforge_profile
86 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
87 __date__ = '$Date: 2008/21/04 $'
88 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
91 #maybe speed up feedRate option
92 def getCraftedText( fileName, gcodeText, stretchRepository = None ):
93 "Stretch a gcode linear move text."
94 return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), stretchRepository )
96 def getCraftedTextFromText( gcodeText, stretchRepository = None ):
97 "Stretch a gcode linear move text."
98 if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'stretch'):
100 if stretchRepository == None:
101 stretchRepository = settings.getReadRepository( StretchRepository() )
102 if not stretchRepository.activateStretch.value:
104 return StretchSkein().getCraftedGcode( gcodeText, stretchRepository )
106 def getNewRepository():
107 'Get new repository.'
108 return StretchRepository()
110 def writeOutput(fileName, shouldAnalyze=True):
111 "Stretch a gcode linear move file. Chain stretch the gcode if it is not already stretched."
112 skeinforge_craft.writeChainTextWithNounMessage(fileName, 'stretch', shouldAnalyze)
115 class LineIteratorBackward:
116 "Backward line iterator class."
117 def __init__( self, isLoop, lineIndex, lines ):
118 self.firstLineIndex = None
120 self.lineIndex = lineIndex
123 def getIndexBeforeNextDeactivate(self):
124 "Get index two lines before the deactivate command."
125 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
126 line = self.lines[lineIndex]
127 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
128 firstWord = gcodec.getFirstWord(splitLine)
129 if firstWord == 'M103':
131 print('This should never happen in stretch, no deactivate command was found for this thread.')
132 raise StopIteration, "You've reached the end of the line."
135 "Get next line going backward or raise exception."
136 while self.lineIndex > 3:
137 if self.lineIndex == self.firstLineIndex:
138 raise StopIteration, "You've reached the end of the line."
139 if self.firstLineIndex == None:
140 self.firstLineIndex = self.lineIndex
141 nextLineIndex = self.lineIndex - 1
142 line = self.lines[self.lineIndex]
143 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
144 firstWord = gcodec.getFirstWord(splitLine)
145 if firstWord == 'M103':
147 nextLineIndex = self.getIndexBeforeNextDeactivate()
149 raise StopIteration, "You've reached the end of the line."
150 if firstWord == 'G1':
151 if self.isBeforeExtrusion():
153 nextLineIndex = self.getIndexBeforeNextDeactivate()
155 raise StopIteration, "You've reached the end of the line."
157 self.lineIndex = nextLineIndex
159 self.lineIndex = nextLineIndex
160 raise StopIteration, "You've reached the end of the line."
162 def isBeforeExtrusion(self):
163 "Determine if index is two or more before activate command."
165 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
166 line = self.lines[lineIndex]
167 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
168 firstWord = gcodec.getFirstWord(splitLine)
169 if firstWord == 'G1':
171 if firstWord == 'M101':
172 return linearMoves > 0
173 if firstWord == 'M103':
175 print('This should never happen in isBeforeExtrusion in stretch, no activate command was found for this thread.')
179 class LineIteratorForward:
180 "Forward line iterator class."
181 def __init__( self, isLoop, lineIndex, lines ):
182 self.firstLineIndex = None
184 self.lineIndex = lineIndex
187 def getIndexJustAfterActivate(self):
188 "Get index just after the activate command."
189 for lineIndex in xrange( self.lineIndex - 1, 3, - 1 ):
190 line = self.lines[lineIndex]
191 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
192 firstWord = gcodec.getFirstWord(splitLine)
193 if firstWord == 'M101':
195 print('This should never happen in stretch, no activate command was found for this thread.')
196 raise StopIteration, "You've reached the end of the line."
199 "Get next line or raise exception."
200 while self.lineIndex < len(self.lines):
201 if self.lineIndex == self.firstLineIndex:
202 raise StopIteration, "You've reached the end of the line."
203 if self.firstLineIndex == None:
204 self.firstLineIndex = self.lineIndex
205 nextLineIndex = self.lineIndex + 1
206 line = self.lines[self.lineIndex]
207 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
208 firstWord = gcodec.getFirstWord(splitLine)
209 if firstWord == 'M103':
211 nextLineIndex = self.getIndexJustAfterActivate()
213 raise StopIteration, "You've reached the end of the line."
214 self.lineIndex = nextLineIndex
215 if firstWord == 'G1':
217 raise StopIteration, "You've reached the end of the line."
220 class StretchRepository:
221 "A class to handle the stretch settings."
223 "Set the default settings, execute title & settings fileName."
224 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.stretch.html', self )
225 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Stretch', self, '')
226 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Stretch')
227 self.activateStretch = settings.BooleanSetting().getFromValue('Activate Stretch', self, False )
228 self.crossLimitDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 3.0, 'Cross Limit Distance Over Perimeter Width (ratio):', self, 10.0, 5.0 )
229 self.loopStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.05, 'Loop Stretch Over Perimeter Width (ratio):', self, 0.25, 0.11 )
230 self.pathStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.0, 'Path Stretch Over Perimeter Width (ratio):', self, 0.2, 0.0 )
231 settings.LabelSeparator().getFromRepository(self)
232 settings.LabelDisplay().getFromName('- Perimeter -', self )
233 self.edgeInsideStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.12, 'Perimeter Inside Stretch Over Perimeter Width (ratio):', self, 0.52, 0.32 )
234 self.edgeOutsideStretchOverEdgeWidth = settings.FloatSpin().getFromValue( 0.05, 'Perimeter Outside Stretch Over Perimeter Width (ratio):', self, 0.25, 0.1 )
235 settings.LabelSeparator().getFromRepository(self)
236 self.stretchFromDistanceOverEdgeWidth = settings.FloatSpin().getFromValue( 1.0, 'Stretch From Distance Over Perimeter Width (ratio):', self, 3.0, 2.0 )
237 self.executeTitle = 'Stretch'
240 "Stretch button has been clicked."
241 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
242 for fileName in fileNames:
243 writeOutput(fileName)
247 "A class to stretch a skein of extrusions."
249 self.distanceFeedRate = gcodec.DistanceFeedRate()
251 self.extruderActive = False
252 self.feedRateMinute = 959.0
254 self.layerCount = settings.LayerCount()
257 self.oldLocation = None
259 def getCraftedGcode( self, gcodeText, stretchRepository ):
260 "Parse gcode text and store the stretch gcode."
261 self.lines = archive.getTextLines(gcodeText)
262 self.stretchRepository = stretchRepository
263 self.parseInitialization()
264 for self.lineIndex in xrange(self.lineIndex, len(self.lines)):
265 line = self.lines[self.lineIndex]
266 self.parseStretch(line)
267 return self.distanceFeedRate.output.getvalue()
269 def getCrossLimitedStretch( self, crossLimitedStretch, crossLineIterator, locationComplex ):
270 "Get cross limited relative stretch for a location."
272 line = crossLineIterator.getNext()
273 except StopIteration:
274 return crossLimitedStretch
275 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
276 pointComplex = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine).dropAxis()
277 pointMinusLocation = locationComplex - pointComplex
278 pointMinusLocationLength = abs( pointMinusLocation )
279 if pointMinusLocationLength <= self.crossLimitDistanceFraction:
280 return crossLimitedStretch
281 parallelNormal = pointMinusLocation / pointMinusLocationLength
282 parallelStretch = euclidean.getDotProduct( parallelNormal, crossLimitedStretch ) * parallelNormal
283 if pointMinusLocationLength > self.crossLimitDistance:
284 return parallelStretch
285 crossNormal = complex( parallelNormal.imag, - parallelNormal.real )
286 crossStretch = euclidean.getDotProduct( crossNormal, crossLimitedStretch ) * crossNormal
287 crossPortion = ( self.crossLimitDistance - pointMinusLocationLength ) / self.crossLimitDistanceRemainder
288 return parallelStretch + crossStretch * crossPortion
290 def getRelativeStretch( self, locationComplex, lineIterator ):
291 "Get relative stretch for a location."
292 lastLocationComplex = locationComplex
294 pointComplex = locationComplex
298 line = lineIterator.getNext()
299 except StopIteration:
300 locationMinusPoint = locationComplex - pointComplex
301 locationMinusPointLength = abs( locationMinusPoint )
302 if locationMinusPointLength > 0.0:
303 return locationMinusPoint / locationMinusPointLength
305 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
306 firstWord = splitLine[0]
307 pointComplex = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine).dropAxis()
308 locationMinusPoint = lastLocationComplex - pointComplex
309 locationMinusPointLength = abs( locationMinusPoint )
310 totalLength += locationMinusPointLength
311 if totalLength >= self.stretchFromDistance:
312 distanceFromRatio = ( self.stretchFromDistance - oldTotalLength ) / locationMinusPointLength
313 totalPoint = distanceFromRatio * pointComplex + ( 1.0 - distanceFromRatio ) * lastLocationComplex
314 locationMinusTotalPoint = locationComplex - totalPoint
315 return locationMinusTotalPoint / self.stretchFromDistance
316 lastLocationComplex = pointComplex
317 oldTotalLength = totalLength
319 def getStretchedLine( self, splitLine ):
320 "Get stretched gcode line."
321 location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
322 self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
323 self.oldLocation = location
324 if self.extruderActive and self.threadMaximumAbsoluteStretch > 0.0:
325 return self.getStretchedLineFromIndexLocation( self.lineIndex - 1, self.lineIndex + 1, location )
326 if self.isJustBeforeExtrusion() and self.threadMaximumAbsoluteStretch > 0.0:
327 return self.getStretchedLineFromIndexLocation( self.lineIndex - 1, self.lineIndex + 1, location )
328 return self.lines[self.lineIndex]
330 def getStretchedLineFromIndexLocation( self, indexPreviousStart, indexNextStart, location ):
331 "Get stretched gcode line from line index and location."
332 crossIteratorForward = LineIteratorForward( self.isLoop, indexNextStart, self.lines )
333 crossIteratorBackward = LineIteratorBackward( self.isLoop, indexPreviousStart, self.lines )
334 iteratorForward = LineIteratorForward( self.isLoop, indexNextStart, self.lines )
335 iteratorBackward = LineIteratorBackward( self.isLoop, indexPreviousStart, self.lines )
336 locationComplex = location.dropAxis()
337 relativeStretch = self.getRelativeStretch( locationComplex, iteratorForward ) + self.getRelativeStretch( locationComplex, iteratorBackward )
338 relativeStretch *= 0.8
339 relativeStretch = self.getCrossLimitedStretch( relativeStretch, crossIteratorForward, locationComplex )
340 relativeStretch = self.getCrossLimitedStretch( relativeStretch, crossIteratorBackward, locationComplex )
341 relativeStretchLength = abs( relativeStretch )
342 if relativeStretchLength > 1.0:
343 relativeStretch /= relativeStretchLength
344 absoluteStretch = relativeStretch * self.threadMaximumAbsoluteStretch
345 stretchedPoint = location.dropAxis() + absoluteStretch
346 return self.distanceFeedRate.getLinearGcodeMovementWithFeedRate( self.feedRateMinute, stretchedPoint, location.z )
348 def isJustBeforeExtrusion(self):
349 "Determine if activate command is before linear move command."
350 for lineIndex in xrange( self.lineIndex + 1, len(self.lines) ):
351 line = self.lines[lineIndex]
352 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
353 firstWord = gcodec.getFirstWord(splitLine)
354 if firstWord == 'G1' or firstWord == 'M103':
356 if firstWord == 'M101':
358 # print('This should never happen in isJustBeforeExtrusion in stretch, no activate or deactivate command was found for this thread.')
361 def parseInitialization(self):
362 'Parse gcode initialization and store the parameters.'
363 for self.lineIndex in xrange(len(self.lines)):
364 line = self.lines[self.lineIndex]
365 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
366 firstWord = gcodec.getFirstWord(splitLine)
367 self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
368 if firstWord == '(</extruderInitialization>)':
369 self.distanceFeedRate.addTagBracketedProcedure('stretch')
371 elif firstWord == '(<edgeWidth>':
372 edgeWidth = float(splitLine[1])
373 self.crossLimitDistance = self.edgeWidth * self.stretchRepository.crossLimitDistanceOverEdgeWidth.value
374 self.loopMaximumAbsoluteStretch = self.edgeWidth * self.stretchRepository.loopStretchOverEdgeWidth.value
375 self.pathAbsoluteStretch = self.edgeWidth * self.stretchRepository.pathStretchOverEdgeWidth.value
376 self.edgeInsideAbsoluteStretch = self.edgeWidth * self.stretchRepository.edgeInsideStretchOverEdgeWidth.value
377 self.edgeOutsideAbsoluteStretch = self.edgeWidth * self.stretchRepository.edgeOutsideStretchOverEdgeWidth.value
378 self.stretchFromDistance = self.stretchRepository.stretchFromDistanceOverEdgeWidth.value * edgeWidth
379 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
380 self.crossLimitDistanceFraction = 0.333333333 * self.crossLimitDistance
381 self.crossLimitDistanceRemainder = self.crossLimitDistance - self.crossLimitDistanceFraction
382 self.distanceFeedRate.addLine(line)
384 def parseStretch(self, line):
385 "Parse a gcode line and add it to the stretch skein."
386 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
387 if len(splitLine) < 1:
389 firstWord = splitLine[0]
390 if firstWord == 'G1':
391 line = self.getStretchedLine(splitLine)
392 elif firstWord == 'M101':
393 self.extruderActive = True
394 elif firstWord == 'M103':
395 self.extruderActive = False
396 self.setStretchToPath()
397 elif firstWord == '(<layer>':
398 self.layerCount.printProgressIncrement('stretch')
399 elif firstWord == '(<loop>':
401 self.threadMaximumAbsoluteStretch = self.loopMaximumAbsoluteStretch
402 elif firstWord == '(</loop>)':
403 self.setStretchToPath()
404 elif firstWord == '(<edge>':
406 self.threadMaximumAbsoluteStretch = self.edgeInsideAbsoluteStretch
407 if splitLine[1] == 'outer':
408 self.threadMaximumAbsoluteStretch = self.edgeOutsideAbsoluteStretch
409 elif firstWord == '(</edge>)':
410 self.setStretchToPath()
411 self.distanceFeedRate.addLine(line)
413 def setStretchToPath(self):
414 "Set the thread stretch to path stretch and is loop false."
416 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
420 "Display the stretch dialog."
421 if len(sys.argv) > 1:
422 writeOutput(' '.join(sys.argv[1 :]))
424 settings.startMainLoopFromConstructor(getNewRepository())
426 if __name__ == "__main__":