chiark / gitweb /
0d29205def73a2d193ffef6ae945036be9ddca4e
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / dimension.py
1 #! /usr/bin/env python
2 """
3 This page is in the table of contents.
4 Dimension adds Adrian's extruder distance E value so firmware does not have to calculate it on it's own and can set the extruder speed in relation to the distance that needs to be extruded.  Some printers don't support this.  Extruder distance is described at:
5
6 http://blog.reprap.org/2009/05/4d-printing.html
7
8 and in Erik de Bruijn's conversion script page at:
9
10 http://objects.reprap.org/wiki/3D-to-5D-Gcode.php
11
12 The dimension manual page is at:
13
14 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Dimension
15
16 Nophead wrote an excellent article on how to set the filament parameters:
17
18 http://hydraraptor.blogspot.com/2011/03/spot-on-flow-rate.html
19
20 ==Operation==
21 The default 'Activate Dimension' 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 ===Extrusion Distance Format Choice===
25 Default is 'Absolute Extrusion Distance' because in Adrian's description the distance is absolute.  In future, because the relative distances are smaller than the cumulative absolute distances, hopefully the firmware will be able to use relative distance.
26
27 ====Absolute Extrusion Distance====
28 When selected, the extrusion distance output will be the total extrusion distance to that gcode line.
29
30 ====Relative Extrusion Distance====
31 When selected, the extrusion distance output will be the extrusion distance from the last gcode line.
32
33 ===Extruder Retraction Speed===
34 Default is 13.3 mm/s.
35
36 Defines the extruder retraction feed rate.  A high value will allow the retraction operation to complete before much material oozes out.  If your extruder can handle it, this value should be much larger than your feed rate.
37
38 As an example, I have a feed rate of 48 mm/s and a 'Extruder Retraction Speed' of 150 mm/s.
39
40 ===Filament===
41 ====Filament Diameter====
42 Default is 2.8 millimeters.
43
44 Defines the filament diameter.
45
46 ====Filament Packing Density====
47 Default is 0.85.  This is for ABS.
48
49 Defines the effective filament packing density.
50
51 The default value is so low for ABS because ABS is relatively soft and with a pinch wheel extruder the teeth of the pinch dig in farther, so it sees a smaller effective diameter.  With a hard plastic like PLA the teeth of the pinch wheel don't dig in as far, so it sees a larger effective diameter, so feeds faster, so for PLA the value should be around 0.97.  This is with Wade's hobbed bolt.  The effect is less significant with larger pinch wheels.
52
53 Overall, you'll have to find the optimal filament packing density by experiment.
54
55 ===Maximum E Value before Reset===
56 Default: 91234.0
57
58 Defines the maximum E value before it is reset with the 'G92 E0' command line.  The reason it is reset only after the maximum E value is reached is because at least one firmware takes time to reset.  The problem with waiting until the E value is high before resetting is that more characters are sent.  So if your firmware takes a lot of time to reset, set this parameter to a high value, if it doesn't set this parameter to a low value or even zero.
59
60 ===Minimum Travel for Retraction===
61 Default: 1.0 millimeter
62
63 Defines the minimum distance that the extruder head has to travel from the end of one thread to the beginning of another, in order to trigger the extruder retraction.  Setting this to a high value means the extruder will retract only occasionally, setting it to a low value means the extruder will retract most of the time.
64
65 ===Retract Within Island===
66 Default is off.
67
68 When selected, retraction will work even when the next thread is within the same island.  If it is not selected, retraction will only work when crossing a boundary.
69
70 ===Retraction Distance===
71 Default is zero.
72
73 Defines the amount the extruder retracts (sucks back) the extruded filament whenever an extruder stop is commanded.  Using this seems to help prevent stringing.  e.g. If set to 10 the extruder reverses the distance required to pull back 10mm of filament.  In fact this does not actually happen but if you set this distance by trial and error you can get to a point where there is very little ooze from the extruder when it stops which is not normally the case. 
74
75 ===Restart Extra Distance===
76 Default is zero.
77
78 Defines the restart extra distance when the thread restarts.  The restart distance will be the retraction distance plus the restart extra distance.
79
80 If this is greater than zero when the extruder starts this distance is added to the retract value giving extra filament.  It can be a negative value in which case it is subtracted from the retraction distance.  On some Repstrap machines a negative value can stop the build up of plastic that can occur at the start of edges.
81
82 ==Examples==
83 The following examples dimension the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and dimension.py.
84
85 > python dimension.py
86 This brings up the dimension dialog.
87
88 > python dimension.py Screw Holder Bottom.stl
89 The dimension tool is parsing the file:
90 Screw Holder Bottom.stl
91 ..
92 The dimension tool has created the file:
93 .. Screw Holder Bottom_dimension.gcode
94
95 """
96 from __future__ import absolute_import
97
98 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
99 from fabmetheus_utilities.geometry.solids import triangle_mesh
100 from fabmetheus_utilities import archive
101 from fabmetheus_utilities import euclidean
102 from fabmetheus_utilities import gcodec
103 from fabmetheus_utilities import intercircle
104 from fabmetheus_utilities import settings
105 from skeinforge_application.skeinforge_utilities import skeinforge_craft
106 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
107 from skeinforge_application.skeinforge_utilities import skeinforge_profile
108 import math
109 import sys
110
111
112 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
113 __date__ = '$Date: 2008/02/05 $'
114 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
115
116
117 def getCraftedText( fileName, gcodeText = '', repository=None):
118         'Dimension a gcode file or text.'
119         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), repository )
120
121 def getCraftedTextFromText(gcodeText, repository=None):
122         'Dimension a gcode text.'
123         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'dimension'):
124                 return gcodeText
125         if repository == None:
126                 repository = settings.getReadRepository( DimensionRepository() )
127         if not repository.activateDimension.value:
128                 return gcodeText
129         return DimensionSkein().getCraftedGcode(gcodeText, repository)
130
131 def getNewRepository():
132         'Get new repository.'
133         return DimensionRepository()
134
135 def writeOutput(fileName, shouldAnalyze=True):
136         'Dimension a gcode file.'
137         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'dimension', shouldAnalyze)
138
139
140 class DimensionRepository(object):
141         'A class to handle the dimension settings.'
142         def __init__(self):
143                 'Set the default settings, execute title & settings fileName.'
144                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.dimension.html', self )
145                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Dimension', self, '')
146                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Dimension')
147                 self.activateDimension = settings.BooleanSetting().getFromValue('Activate Dimension', self, True )
148                 extrusionDistanceFormatLatentStringVar = settings.LatentStringVar()
149                 self.extrusionDistanceFormatChoiceLabel = settings.LabelDisplay().getFromName('Extrusion Distance Format Choice: ', self )
150                 settings.Radio().getFromRadio( extrusionDistanceFormatLatentStringVar, 'Absolute Extrusion Distance', self, True )
151                 self.relativeExtrusionDistance = settings.Radio().getFromRadio( extrusionDistanceFormatLatentStringVar, 'Relative Extrusion Distance', self, False )
152                 self.extruderRetractionSpeed = settings.FloatSpin().getFromValue( 4.0, 'Extruder Retraction Speed (mm/s):', self, 34.0, 13.3 )
153                 settings.LabelSeparator().getFromRepository(self)
154                 settings.LabelDisplay().getFromName('- Filament -', self )
155                 self.filamentDiameter = settings.FloatSpin().getFromValue(1.0, 'Filament Diameter (mm):', self, 6.0, 2.89)
156                 self.filamentPackingDensity = settings.FloatSpin().getFromValue(0.7, 'Filament Packing Density (ratio):', self, 1.0, 1.0)
157                 settings.LabelSeparator().getFromRepository(self)
158                 self.maximumEValueBeforeReset = settings.FloatSpin().getFromValue(0.0, 'Maximum E Value before Reset (float):', self, 999999.9, 91234.0)
159                 self.minimumTravelForRetraction = settings.FloatSpin().getFromValue(0.0, 'Minimum Travel for Retraction (millimeters):', self, 2.0, 1.0)
160                 self.retractWithinIsland = settings.BooleanSetting().getFromValue('Retract Within Island', self, False)
161                 self.retractionDistance = settings.FloatSpin().getFromValue( 0.0, 'Retraction Distance (millimeters):', self, 100.0, 0.0 )
162                 self.restartExtraDistance = settings.FloatSpin().getFromValue( 0.0, 'Restart Extra Distance (millimeters):', self, 100.0, 0.0 )
163                 self.executeTitle = 'Dimension'
164
165         def execute(self):
166                 'Dimension button has been clicked.'
167                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
168                 for fileName in fileNames:
169                         writeOutput(fileName)
170
171
172 class DimensionSkein(object):
173         'A class to dimension a skein of extrusions.'
174         def __init__(self):
175                 'Initialize.'
176                 self.absoluteDistanceMode = True
177                 self.boundaryLayers = []
178                 self.distanceFeedRate = gcodec.DistanceFeedRate()
179                 self.feedRateMinute = None
180                 self.isExtruderActive = False
181                 self.layerIndex = -1
182                 self.lineIndex = 0
183                 self.maximumZFeedRatePerSecond = None
184                 self.oldLocation = None
185                 self.operatingFlowRate = None
186                 self.retractionRatio = 1.0
187                 self.totalExtrusionDistance = 0.0
188                 self.travelFeedRatePerSecond = None
189                 self.addRetraction = True
190                 self.reverseRetraction = False
191                 self.maxDistancePerMove = 30
192
193         def addLinearMoveExtrusionDistanceLine(self, extrusionDistance):
194                 'Get the extrusion distance string from the extrusion distance.'
195                 if self.repository.extruderRetractionSpeed.value != 0.0 and extrusionDistance != 0.0:
196                         self.distanceFeedRate.output.write('G1 F%s\n' % self.extruderRetractionSpeedMinuteString)
197                         self.distanceFeedRate.output.write('G1%s\n' % self.getExtrusionDistanceStringFromExtrusionDistance(extrusionDistance))
198                         self.distanceFeedRate.output.write('G1 F%s\n' % self.distanceFeedRate.getRounded(self.feedRateMinute))
199
200         def getCraftedGcode(self, gcodeText, repository):
201                 'Parse gcode text and store the dimension gcode.'
202                 self.repository = repository
203                 filamentRadius = 0.5 * repository.filamentDiameter.value
204                 filamentPackingArea = math.pi * filamentRadius * filamentRadius * repository.filamentPackingDensity.value
205                 self.minimumTravelForRetraction = self.repository.minimumTravelForRetraction.value
206                 self.doubleMinimumTravelForRetraction = self.minimumTravelForRetraction + self.minimumTravelForRetraction
207                 self.lines = archive.getTextLines(gcodeText)
208                 self.parseInitialization()
209                 if not self.repository.retractWithinIsland.value:
210                         self.parseBoundaries()
211                 self.flowScaleSixty = 60.0 * self.layerHeight * self.edgeWidth / filamentPackingArea
212                 self.restartDistance = self.repository.retractionDistance.value + self.repository.restartExtraDistance.value
213                 self.extruderRetractionSpeedMinuteString = self.distanceFeedRate.getRounded(60.0 * self.repository.extruderRetractionSpeed.value)
214                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
215                         self.parseLine( lineIndex )
216                 return self.distanceFeedRate.output.getvalue()
217
218         def getDimensionedArcMovement(self, line, splitLine):
219                 'Get a dimensioned arc movement.'
220                 if self.oldLocation == None:
221                         return line
222                 relativeLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
223                 self.oldLocation += relativeLocation
224                 distance = gcodec.getArcDistance(relativeLocation, splitLine)
225                 return line + self.getExtrusionDistanceString(distance, splitLine)
226
227         def getDimensionedLinearMovement( self, line, splitLine ):
228                 'Get a dimensioned linear movement.'
229                 distance = 0.0
230                 if self.absoluteDistanceMode:
231                         location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
232                         if self.oldLocation != None:
233                                 distance = abs( location - self.oldLocation )
234                                 if distance > self.maxDistancePerMove * 1.1:
235                                         extra = ''
236                                         while distance > self.maxDistancePerMove * 1.1:
237                                                 self.oldLocation.z = location.z
238                                                 self.oldLocation += (location - self.oldLocation) / distance * self.maxDistancePerMove
239                                                 distance -= self.maxDistancePerMove
240                                                 e = self.getExtrusionDistanceString(self.maxDistancePerMove, splitLine)
241                                                 extra += self.distanceFeedRate.getLinearGcodeMovementWithFeedRate(self.feedRateMinute, self.oldLocation.dropAxis(), self.oldLocation.z) + e + '\n'
242                                         line = extra + line
243                         self.oldLocation = location
244                 else:
245                         if self.oldLocation is None:
246                                 print('Warning: There was no absolute location when the G91 command was parsed, so the absolute location will be set to the origin.')
247                                 self.oldLocation = Vector3()
248                         location = gcodec.getLocationFromSplitLine(None, splitLine)
249                         distance = abs( location )
250                         self.oldLocation += location
251                 return line + self.getExtrusionDistanceString( distance, splitLine )
252
253         def getDistanceToNextThread(self, lineIndex):
254                 'Get the travel distance to the next thread.'
255                 if self.oldLocation == None:
256                         return None
257                 isActive = False
258                 location = self.oldLocation
259                 for afterIndex in xrange(lineIndex + 1, len(self.lines)):
260                         line = self.lines[afterIndex]
261                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
262                         firstWord = gcodec.getFirstWord(splitLine)
263                         if firstWord == 'G1':
264                                 if isActive:
265                                         if not self.repository.retractWithinIsland.value:
266                                                 locationEnclosureIndex = self.getSmallestEnclosureIndex(location.dropAxis())
267                                                 if locationEnclosureIndex == self.getSmallestEnclosureIndex(self.oldLocation.dropAxis()):
268                                                         return None
269                                         locationMinusOld = location - self.oldLocation
270                                         return abs(locationMinusOld)
271                                 location = gcodec.getLocationFromSplitLine(location, splitLine)
272                         elif firstWord == 'M101':
273                                 isActive = True
274                         elif firstWord == 'M103':
275                                 isActive = False
276                 return None
277
278         def getExtrusionDistanceString( self, distance, splitLine ):
279                 'Get the extrusion distance string.'
280                 self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
281                 if not self.isExtruderActive:
282                         return ''
283                 if distance == 0.0:
284                         return ''
285                 if distance < 0.0:
286                         print('Warning, the distance is less than zero in getExtrusionDistanceString in dimension; so there will not be an E value')
287                         print(distance)
288                         print(splitLine)
289                         return ''
290                 if self.operatingFlowRate == None:
291                         return self.getExtrusionDistanceStringFromExtrusionDistance(self.flowScaleSixty / 60.0 * distance)
292                 else:
293                         scaledFlowRate = self.flowRate * self.flowScaleSixty
294                         return self.getExtrusionDistanceStringFromExtrusionDistance(scaledFlowRate / self.feedRateMinute * distance)
295
296         def getExtrusionDistanceStringFromExtrusionDistance(self, extrusionDistance):
297                 'Get the extrusion distance string from the extrusion distance.'
298                 if self.repository.relativeExtrusionDistance.value:
299                         return ' E' + self.distanceFeedRate.getRounded(extrusionDistance)
300                 self.totalExtrusionDistance += extrusionDistance
301                 return ' E' + self.distanceFeedRate.getRounded(self.totalExtrusionDistance)
302
303         def getRetractionRatio(self, lineIndex):
304                 'Get the retraction ratio.'
305                 distanceToNextThread = self.getDistanceToNextThread(lineIndex)
306                 if distanceToNextThread == None:
307                         return 0.0
308                 if distanceToNextThread >= self.doubleMinimumTravelForRetraction:
309                         return 1.0
310                 if distanceToNextThread <= self.minimumTravelForRetraction:
311                         return 0.0
312                 return (distanceToNextThread - self.minimumTravelForRetraction) / self.minimumTravelForRetraction
313
314         def getSmallestEnclosureIndex(self, point):
315                 'Get the index of the smallest boundary loop which encloses the point.'
316                 boundaryLayer = self.boundaryLayers[self.layerIndex]
317                 for loopIndex, loop in enumerate(boundaryLayer.loops):
318                         if euclidean.isPointInsideLoop(loop, point):
319                                 return loopIndex
320                 return None
321
322         def parseBoundaries(self):
323                 'Parse the boundaries and add them to the boundary layers.'
324                 boundaryLoop = None
325                 boundaryLayer = None
326                 for line in self.lines[self.lineIndex :]:
327                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
328                         firstWord = gcodec.getFirstWord(splitLine)
329                         if firstWord == '(</boundaryPerimeter>)':
330                                 boundaryLoop = None
331                         elif firstWord == '(<boundaryPoint>':
332                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
333                                 if boundaryLoop == None:
334                                         boundaryLoop = []
335                                         boundaryLayer.loops.append(boundaryLoop)
336                                 boundaryLoop.append(location.dropAxis())
337                         elif firstWord == '(<layer>':
338                                 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
339                                 self.boundaryLayers.append(boundaryLayer)
340                 for boundaryLayer in self.boundaryLayers:
341                         triangle_mesh.sortLoopsInOrderOfArea(False, boundaryLayer.loops)
342
343         def parseInitialization(self):
344                 'Parse gcode initialization and store the parameters.'
345                 for self.lineIndex in xrange(len(self.lines)):
346                         line = self.lines[self.lineIndex]
347                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
348                         firstWord = gcodec.getFirstWord(splitLine)
349                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
350                         if firstWord == '(</extruderInitialization>)':
351                                 self.distanceFeedRate.addTagBracketedProcedure('dimension')
352                                 return
353                         elif firstWord == '(<layerHeight>':
354                                 self.layerHeight = float(splitLine[1])
355                         elif firstWord == '(<maximumZDrillFeedRatePerSecond>':
356                                 self.maximumZFeedRatePerSecond = float(splitLine[1])
357                         elif firstWord == '(<maximumZFeedRatePerSecond>':
358                                 self.maximumZFeedRatePerSecond = float(splitLine[1])
359                         elif firstWord == '(<operatingFeedRatePerSecond>':
360                                 self.feedRateMinute = 60.0 * float(splitLine[1])
361                         elif firstWord == '(<operatingFlowRate>':
362                                 self.operatingFlowRate = float(splitLine[1])
363                                 self.flowRate = self.operatingFlowRate
364                         elif firstWord == '(<edgeWidth>':
365                                 self.edgeWidth = float(splitLine[1])
366                         elif firstWord == '(<travelFeedRatePerSecond>':
367                                 self.travelFeedRatePerSecond = float(splitLine[1])
368                         self.distanceFeedRate.addLine(line)
369
370         def parseLine( self, lineIndex ):
371                 'Parse a gcode line and add it to the dimension skein.'
372                 line = self.lines[lineIndex].lstrip()
373                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
374                 if len(splitLine) < 1:
375                         return
376                 firstWord = splitLine[0]
377                 if firstWord == 'G2' or firstWord == 'G3':
378                         line = self.getDimensionedArcMovement( line, splitLine )
379                 if firstWord == 'G1':
380                         line = self.getDimensionedLinearMovement( line, splitLine )
381                 if firstWord == 'G90':
382                         self.absoluteDistanceMode = True
383                 elif firstWord == 'G91':
384                         self.absoluteDistanceMode = False
385                 elif firstWord == '(<layer>':
386                         self.layerIndex += 1
387                         settings.printProgress(self.layerIndex, 'dimension')
388                 elif firstWord == '(</layer>)' or firstWord == '(<supportLayer>)' or firstWord == '(</supportLayer>)':
389                         if self.totalExtrusionDistance > 0.0 and not self.repository.relativeExtrusionDistance.value:
390                                 self.distanceFeedRate.addLine('G92 E0')
391                                 self.totalExtrusionDistance = 0.0
392                 elif firstWord == 'M101':
393                         if self.retractionRatio > 0.0:
394                                 self.addLinearMoveExtrusionDistanceLine(self.restartDistance * self.retractionRatio)
395                         if self.totalExtrusionDistance > self.repository.maximumEValueBeforeReset.value: 
396                                 if not self.repository.relativeExtrusionDistance.value:
397                                         self.distanceFeedRate.addLine('G92 E0')
398                                         self.totalExtrusionDistance = 0.0
399                         self.isExtruderActive = True
400                 elif firstWord == 'M103':
401                         self.retractionRatio = self.getRetractionRatio(lineIndex)
402                         if self.retractionRatio > 0.0:
403                                 self.addLinearMoveExtrusionDistanceLine(-self.repository.retractionDistance.value * self.retractionRatio)
404                         self.isExtruderActive = False
405                 elif firstWord == 'M108':
406                         self.flowRate = float( splitLine[1][1 :] )
407                 self.distanceFeedRate.addLine(line)
408
409
410 def main():
411         'Display the dimension dialog.'
412         if len(sys.argv) > 1:
413                 writeOutput(' '.join(sys.argv[1 :]))
414         else:
415                 settings.startMainLoopFromConstructor(getNewRepository())
416
417 if __name__ == '__main__':
418         main()