chiark / gitweb /
Move SF into its own directory, to seperate SF and Cura. Rename newui to gui.
[cura.git] / Cura / 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 #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.
71 import __init__
72
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
83 import sys
84
85
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'
89
90
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 )
95
96 def getCraftedTextFromText( gcodeText, stretchRepository = None ):
97         "Stretch a gcode linear move text."
98         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'stretch'):
99                 return gcodeText
100         if stretchRepository == None:
101                 stretchRepository = settings.getReadRepository( StretchRepository() )
102         if not stretchRepository.activateStretch.value:
103                 return gcodeText
104         return StretchSkein().getCraftedGcode( gcodeText, stretchRepository )
105
106 def getNewRepository():
107         'Get new repository.'
108         return StretchRepository()
109
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)
113
114
115 class LineIteratorBackward:
116         "Backward line iterator class."
117         def __init__( self, isLoop, lineIndex, lines ):
118                 self.firstLineIndex = None
119                 self.isLoop = isLoop
120                 self.lineIndex = lineIndex
121                 self.lines = lines
122
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':
130                                 return lineIndex - 2
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."
133
134         def getNext(self):
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':
146                                 if self.isLoop:
147                                         nextLineIndex = self.getIndexBeforeNextDeactivate()
148                                 else:
149                                         raise StopIteration, "You've reached the end of the line."
150                         if firstWord == 'G1':
151                                 if self.isBeforeExtrusion():
152                                         if self.isLoop:
153                                                 nextLineIndex = self.getIndexBeforeNextDeactivate()
154                                         else:
155                                                 raise StopIteration, "You've reached the end of the line."
156                                 else:
157                                         self.lineIndex = nextLineIndex
158                                         return line
159                         self.lineIndex = nextLineIndex
160                 raise StopIteration, "You've reached the end of the line."
161
162         def isBeforeExtrusion(self):
163                 "Determine if index is two or more before activate command."
164                 linearMoves = 0
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':
170                                 linearMoves += 1
171                         if firstWord == 'M101':
172                                 return linearMoves > 0
173                         if firstWord == 'M103':
174                                 return False
175                 print('This should never happen in isBeforeExtrusion in stretch, no activate command was found for this thread.')
176                 return False
177
178
179 class LineIteratorForward:
180         "Forward line iterator class."
181         def __init__( self, isLoop, lineIndex, lines ):
182                 self.firstLineIndex = None
183                 self.isLoop = isLoop
184                 self.lineIndex = lineIndex
185                 self.lines = lines
186
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':
194                                 return lineIndex + 1
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."
197
198         def getNext(self):
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':
210                                 if self.isLoop:
211                                         nextLineIndex = self.getIndexJustAfterActivate()
212                                 else:
213                                         raise StopIteration, "You've reached the end of the line."
214                         self.lineIndex = nextLineIndex
215                         if firstWord == 'G1':
216                                 return line
217                 raise StopIteration, "You've reached the end of the line."
218
219
220 class StretchRepository:
221         "A class to handle the stretch settings."
222         def __init__(self):
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'
238
239         def execute(self):
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)
244
245
246 class StretchSkein:
247         "A class to stretch a skein of extrusions."
248         def __init__(self):
249                 self.distanceFeedRate = gcodec.DistanceFeedRate()
250                 self.edgeWidth = 0.4
251                 self.extruderActive = False
252                 self.feedRateMinute = 959.0
253                 self.isLoop = False
254                 self.layerCount = settings.LayerCount()
255                 self.lineIndex = 0
256                 self.lines = None
257                 self.oldLocation = None
258
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()
268
269         def getCrossLimitedStretch( self, crossLimitedStretch, crossLineIterator, locationComplex ):
270                 "Get cross limited relative stretch for a location."
271                 try:
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
289
290         def getRelativeStretch( self, locationComplex, lineIterator ):
291                 "Get relative stretch for a location."
292                 lastLocationComplex = locationComplex
293                 oldTotalLength = 0.0
294                 pointComplex = locationComplex
295                 totalLength = 0.0
296                 while 1:
297                         try:
298                                 line = lineIterator.getNext()
299                         except StopIteration:
300                                 locationMinusPoint = locationComplex - pointComplex
301                                 locationMinusPointLength = abs( locationMinusPoint )
302                                 if locationMinusPointLength > 0.0:
303                                         return locationMinusPoint / locationMinusPointLength
304                                 return complex()
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
318
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]
329
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 )
347
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':
355                                 return False
356                         if firstWord == 'M101':
357                                 return True
358 #               print('This should never happen in isJustBeforeExtrusion in stretch, no activate or deactivate command was found for this thread.')
359                 return False
360
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')
370                                 return
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)
383
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:
388                         return
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>':
400                         self.isLoop = True
401                         self.threadMaximumAbsoluteStretch = self.loopMaximumAbsoluteStretch
402                 elif firstWord == '(</loop>)':
403                         self.setStretchToPath()
404                 elif firstWord == '(<edge>':
405                         self.isLoop = True
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)
412
413         def setStretchToPath(self):
414                 "Set the thread stretch to path stretch and is loop false."
415                 self.isLoop = False
416                 self.threadMaximumAbsoluteStretch = self.pathAbsoluteStretch
417
418
419 def main():
420         "Display the stretch dialog."
421         if len(sys.argv) > 1:
422                 writeOutput(' '.join(sys.argv[1 :]))
423         else:
424                 settings.startMainLoopFromConstructor(getNewRepository())
425
426 if __name__ == "__main__":
427         main()