chiark / gitweb /
Add uppercase STL and HEX to file dialog filters for linux/MacOS
[cura.git] / Cura / 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
97 #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.
98 import __init__
99
100 from datetime import date
101 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
102 from fabmetheus_utilities.geometry.solids import triangle_mesh
103 from fabmetheus_utilities import archive
104 from fabmetheus_utilities import euclidean
105 from fabmetheus_utilities import gcodec
106 from fabmetheus_utilities import intercircle
107 from fabmetheus_utilities import settings
108 from skeinforge_application.skeinforge_utilities import skeinforge_craft
109 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
110 from skeinforge_application.skeinforge_utilities import skeinforge_profile
111 import math
112 import os
113 import sys
114
115
116 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
117 __date__ = '$Date: 2008/02/05 $'
118 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
119
120
121 def getCraftedText( fileName, gcodeText = '', repository=None):
122         'Dimension a gcode file or text.'
123         return getCraftedTextFromText( archive.getTextIfEmpty(fileName, gcodeText), repository )
124
125 def getCraftedTextFromText(gcodeText, repository=None):
126         'Dimension a gcode text.'
127         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'dimension'):
128                 return gcodeText
129         if repository == None:
130                 repository = settings.getReadRepository( DimensionRepository() )
131         if not repository.activateDimension.value:
132                 return gcodeText
133         return DimensionSkein().getCraftedGcode(gcodeText, repository)
134
135 def getNewRepository():
136         'Get new repository.'
137         return DimensionRepository()
138
139 def writeOutput(fileName, shouldAnalyze=True):
140         'Dimension a gcode file.'
141         skeinforge_craft.writeChainTextWithNounMessage(fileName, 'dimension', shouldAnalyze)
142
143
144 class DimensionRepository:
145         'A class to handle the dimension settings.'
146         def __init__(self):
147                 'Set the default settings, execute title & settings fileName.'
148                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.dimension.html', self )
149                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Dimension', self, '')
150                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Dimension')
151                 self.activateDimension = settings.BooleanSetting().getFromValue('Activate Dimension', self, True )
152                 extrusionDistanceFormatLatentStringVar = settings.LatentStringVar()
153                 self.extrusionDistanceFormatChoiceLabel = settings.LabelDisplay().getFromName('Extrusion Distance Format Choice: ', self )
154                 settings.Radio().getFromRadio( extrusionDistanceFormatLatentStringVar, 'Absolute Extrusion Distance', self, True )
155                 self.relativeExtrusionDistance = settings.Radio().getFromRadio( extrusionDistanceFormatLatentStringVar, 'Relative Extrusion Distance', self, False )
156                 self.extruderRetractionSpeed = settings.FloatSpin().getFromValue( 4.0, 'Extruder Retraction Speed (mm/s):', self, 34.0, 13.3 )
157                 settings.LabelSeparator().getFromRepository(self)
158                 settings.LabelDisplay().getFromName('- Filament -', self )
159                 self.filamentDiameter = settings.FloatSpin().getFromValue(1.0, 'Filament Diameter (mm):', self, 6.0, 2.89)
160                 self.filamentPackingDensity = settings.FloatSpin().getFromValue(0.7, 'Filament Packing Density (ratio):', self, 1.0, 1.0)
161                 settings.LabelSeparator().getFromRepository(self)
162                 self.maximumEValueBeforeReset = settings.FloatSpin().getFromValue(0.0, 'Maximum E Value before Reset (float):', self, 999999.9, 91234.0)
163                 self.minimumTravelForRetraction = settings.FloatSpin().getFromValue(0.0, 'Minimum Travel for Retraction (millimeters):', self, 2.0, 1.0)
164                 self.retractWithinIsland = settings.BooleanSetting().getFromValue('Retract Within Island', self, False)
165                 self.retractionDistance = settings.FloatSpin().getFromValue( 0.0, 'Retraction Distance (millimeters):', self, 100.0, 0.0 )
166                 self.restartExtraDistance = settings.FloatSpin().getFromValue( 0.0, 'Restart Extra Distance (millimeters):', self, 100.0, 0.0 )
167                 self.executeTitle = 'Dimension'
168
169         def execute(self):
170                 'Dimension button has been clicked.'
171                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
172                 for fileName in fileNames:
173                         writeOutput(fileName)
174
175
176 class DimensionSkein:
177         'A class to dimension a skein of extrusions.'
178         def __init__(self):
179                 'Initialize.'
180                 self.absoluteDistanceMode = True
181                 self.boundaryLayers = []
182                 self.distanceFeedRate = gcodec.DistanceFeedRate()
183                 self.feedRateMinute = None
184                 self.isExtruderActive = False
185                 self.layerIndex = -1
186                 self.lineIndex = 0
187                 self.maximumZFeedRatePerSecond = None
188                 self.oldLocation = None
189                 self.operatingFlowRate = None
190                 self.retractionRatio = 1.0
191                 self.totalExtrusionDistance = 0.0
192                 self.travelFeedRatePerSecond = None
193                 self.zDistanceRatio = 5.0
194
195         def addLinearMoveExtrusionDistanceLine(self, extrusionDistance):
196                 'Get the extrusion distance string from the extrusion distance.'
197                 if self.repository.extruderRetractionSpeed.value != 0.0 and extrusionDistance != 0.0:
198                         self.distanceFeedRate.output.write('G1 F%s\n' % self.extruderRetractionSpeedMinuteString)
199                         self.distanceFeedRate.output.write('G1%s\n' % self.getExtrusionDistanceStringFromExtrusionDistance(extrusionDistance))
200                         self.distanceFeedRate.output.write('G1 F%s\n' % self.distanceFeedRate.getRounded(self.feedRateMinute))
201
202         def getCraftedGcode(self, gcodeText, repository):
203                 'Parse gcode text and store the dimension gcode.'
204                 self.repository = repository
205                 filamentRadius = 0.5 * repository.filamentDiameter.value
206                 filamentPackingArea = math.pi * filamentRadius * filamentRadius * repository.filamentPackingDensity.value
207                 self.minimumTravelForRetraction = self.repository.minimumTravelForRetraction.value
208                 self.doubleMinimumTravelForRetraction = self.minimumTravelForRetraction + self.minimumTravelForRetraction
209                 self.lines = archive.getTextLines(gcodeText)
210                 self.parseInitialization()
211                 if not self.repository.retractWithinIsland.value:
212                         self.parseBoundaries()
213                 self.flowScaleSixty = 60.0 * self.layerHeight * self.edgeWidth / filamentPackingArea
214                 self.restartDistance = self.repository.retractionDistance.value + self.repository.restartExtraDistance.value
215                 self.extruderRetractionSpeedMinuteString = self.distanceFeedRate.getRounded(60.0 * self.repository.extruderRetractionSpeed.value)
216                 if self.maximumZFeedRatePerSecond != None and self.travelFeedRatePerSecond != None:
217                         self.zDistanceRatio = self.travelFeedRatePerSecond / self.maximumZFeedRatePerSecond
218                 for lineIndex in xrange(self.lineIndex, len(self.lines)):
219                         self.parseLine( lineIndex )
220                 return self.distanceFeedRate.output.getvalue()
221
222         def getDimensionedArcMovement(self, line, splitLine):
223                 'Get a dimensioned arc movement.'
224                 if self.oldLocation == None:
225                         return line
226                 relativeLocation = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
227                 self.oldLocation += relativeLocation
228                 distance = gcodec.getArcDistance(relativeLocation, splitLine)
229                 return line + self.getExtrusionDistanceString(distance, splitLine)
230
231         def getDimensionedLinearMovement( self, line, splitLine ):
232                 'Get a dimensioned linear movement.'
233                 distance = 0.0
234                 if self.absoluteDistanceMode:
235                         location = gcodec.getLocationFromSplitLine(self.oldLocation, splitLine)
236                         if self.oldLocation != None:
237                                 distance = abs( location - self.oldLocation )
238                         self.oldLocation = location
239                 else:
240                         if self.oldLocation == None:
241                                 print('Warning: There was no absolute location when the G91 command was parsed, so the absolute location will be set to the origin.')
242                                 self.oldLocation = Vector3()
243                         location = gcodec.getLocationFromSplitLine(None, splitLine)
244                         distance = abs( location )
245                         self.oldLocation += location
246                 return line + self.getExtrusionDistanceString( distance, splitLine )
247
248         def getDistanceToNextThread(self, lineIndex):
249                 'Get the travel distance to the next thread.'
250                 if self.oldLocation == None:
251                         return None
252                 isActive = False
253                 location = self.oldLocation
254                 for afterIndex in xrange(lineIndex + 1, len(self.lines)):
255                         line = self.lines[afterIndex]
256                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
257                         firstWord = gcodec.getFirstWord(splitLine)
258                         if firstWord == 'G1':
259                                 if isActive:
260                                         if not self.repository.retractWithinIsland.value:
261                                                 locationEnclosureIndex = self.getSmallestEnclosureIndex(location.dropAxis())
262                                                 if locationEnclosureIndex != self.getSmallestEnclosureIndex(self.oldLocation.dropAxis()):
263                                                         return None
264                                         locationMinusOld = location - self.oldLocation
265                                         xyTravel = abs(locationMinusOld.dropAxis())
266                                         zTravelMultiplied = locationMinusOld.z * self.zDistanceRatio
267                                         return math.sqrt(xyTravel * xyTravel + zTravelMultiplied * zTravelMultiplied)
268                                 location = gcodec.getLocationFromSplitLine(location, splitLine)
269                         elif firstWord == 'M101':
270                                 isActive = True
271                         elif firstWord == 'M103':
272                                 isActive = False
273                 return None
274
275         def getExtrusionDistanceString( self, distance, splitLine ):
276                 'Get the extrusion distance string.'
277                 self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
278                 if not self.isExtruderActive:
279                         return ''
280                 if distance == 0.0:
281                         return ''
282                 if distance < 0.0:
283                         print('Warning, the distance is less than zero in getExtrusionDistanceString in dimension; so there will not be an E value')
284                         print(distance)
285                         print(splitLine)
286                         return ''
287                 if self.operatingFlowRate == None:
288                         return self.getExtrusionDistanceStringFromExtrusionDistance(self.flowScaleSixty / 60.0 * distance)
289                 else:
290                         scaledFlowRate = self.flowRate * self.flowScaleSixty
291                         return self.getExtrusionDistanceStringFromExtrusionDistance(scaledFlowRate / self.feedRateMinute * distance)
292
293         def getExtrusionDistanceStringFromExtrusionDistance(self, extrusionDistance):
294                 'Get the extrusion distance string from the extrusion distance.'
295                 if self.repository.relativeExtrusionDistance.value:
296                         return ' E' + self.distanceFeedRate.getRounded(extrusionDistance)
297                 self.totalExtrusionDistance += extrusionDistance
298                 return ' E' + self.distanceFeedRate.getRounded(self.totalExtrusionDistance)
299
300         def getRetractionRatio(self, lineIndex):
301                 'Get the retraction ratio.'
302                 distanceToNextThread = self.getDistanceToNextThread(lineIndex)
303                 if distanceToNextThread == None:
304                         return 1.0
305                 if distanceToNextThread >= self.doubleMinimumTravelForRetraction:
306                         return 1.0
307                 if distanceToNextThread <= self.minimumTravelForRetraction:
308                         return 0.0
309                 return (distanceToNextThread - self.minimumTravelForRetraction) / self.minimumTravelForRetraction
310
311         def getSmallestEnclosureIndex(self, point):
312                 'Get the index of the smallest boundary loop which encloses the point.'
313                 boundaryLayer = self.boundaryLayers[self.layerIndex]
314                 for loopIndex, loop in enumerate(boundaryLayer.loops):
315                         if euclidean.isPointInsideLoop(loop, point):
316                                 return loopIndex
317                 return None
318
319         def parseBoundaries(self):
320                 'Parse the boundaries and add them to the boundary layers.'
321                 boundaryLoop = None
322                 boundaryLayer = None
323                 for line in self.lines[self.lineIndex :]:
324                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
325                         firstWord = gcodec.getFirstWord(splitLine)
326                         if firstWord == '(</boundaryPerimeter>)':
327                                 boundaryLoop = None
328                         elif firstWord == '(<boundaryPoint>':
329                                 location = gcodec.getLocationFromSplitLine(None, splitLine)
330                                 if boundaryLoop == None:
331                                         boundaryLoop = []
332                                         boundaryLayer.loops.append(boundaryLoop)
333                                 boundaryLoop.append(location.dropAxis())
334                         elif firstWord == '(<layer>':
335                                 boundaryLayer = euclidean.LoopLayer(float(splitLine[1]))
336                                 self.boundaryLayers.append(boundaryLayer)
337                 for boundaryLayer in self.boundaryLayers:
338                         triangle_mesh.sortLoopsInOrderOfArea(False, boundaryLayer.loops)
339
340         def parseInitialization(self):
341                 'Parse gcode initialization and store the parameters.'
342                 for self.lineIndex in xrange(len(self.lines)):
343                         line = self.lines[self.lineIndex]
344                         splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
345                         firstWord = gcodec.getFirstWord(splitLine)
346                         self.distanceFeedRate.parseSplitLine(firstWord, splitLine)
347                         if firstWord == '(</extruderInitialization>)':
348                                 self.distanceFeedRate.addTagBracketedProcedure('dimension')
349                                 return
350                         elif firstWord == '(<layerHeight>':
351                                 self.layerHeight = float(splitLine[1])
352                         elif firstWord == '(<maximumZDrillFeedRatePerSecond>':
353                                 self.maximumZFeedRatePerSecond = float(splitLine[1])
354                         elif firstWord == '(<maximumZFeedRatePerSecond>':
355                                 self.maximumZFeedRatePerSecond = float(splitLine[1])
356                         elif firstWord == '(<operatingFeedRatePerSecond>':
357                                 self.feedRateMinute = 60.0 * float(splitLine[1])
358                         elif firstWord == '(<operatingFlowRate>':
359                                 self.operatingFlowRate = float(splitLine[1])
360                                 self.flowRate = self.operatingFlowRate
361                         elif firstWord == '(<edgeWidth>':
362                                 self.edgeWidth = float(splitLine[1])
363                         elif firstWord == '(<travelFeedRatePerSecond>':
364                                 self.travelFeedRatePerSecond = float(splitLine[1])
365                         self.distanceFeedRate.addLine(line)
366
367         def parseLine( self, lineIndex ):
368                 'Parse a gcode line and add it to the dimension skein.'
369                 line = self.lines[lineIndex].lstrip()
370                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
371                 if len(splitLine) < 1:
372                         return
373                 firstWord = splitLine[0]
374                 if firstWord == 'G2' or firstWord == 'G3':
375                         line = self.getDimensionedArcMovement( line, splitLine )
376                 if firstWord == 'G1':
377                         line = self.getDimensionedLinearMovement( line, splitLine )
378                 if firstWord == 'G90':
379                         self.absoluteDistanceMode = True
380                 elif firstWord == 'G91':
381                         self.absoluteDistanceMode = False
382                 elif firstWord == '(<layer>':
383                         self.layerIndex += 1
384                         settings.printProgress(self.layerIndex, 'dimension')
385                 elif firstWord == 'M101':
386                         self.addLinearMoveExtrusionDistanceLine(self.restartDistance * self.retractionRatio)
387                         if self.totalExtrusionDistance > self.repository.maximumEValueBeforeReset.value: 
388                                 if not self.repository.relativeExtrusionDistance.value:
389                                         self.distanceFeedRate.addLine('G92 E0')
390                                         self.totalExtrusionDistance = 0.0
391                         self.isExtruderActive = True
392                 elif firstWord == 'M103':
393                         self.retractionRatio = self.getRetractionRatio(lineIndex)
394                         self.addLinearMoveExtrusionDistanceLine(-self.repository.retractionDistance.value * self.retractionRatio)
395                         self.isExtruderActive = False
396                 elif firstWord == 'M108':
397                         self.flowRate = float( splitLine[1][1 :] )
398                 self.distanceFeedRate.addLine(line)
399
400
401 def main():
402         'Display the dimension dialog.'
403         if len(sys.argv) > 1:
404                 writeOutput(' '.join(sys.argv[1 :]))
405         else:
406                 settings.startMainLoopFromConstructor(getNewRepository())
407
408 if __name__ == '__main__':
409         main()