chiark / gitweb /
Add back the ultimaker platform, and made the platform mesh simpler.
[cura.git] / Cura / slice / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / export.py
1 """
2 This page is in the table of contents.
3 Export is a craft tool to pick an export plugin, add information to the file name, and delete comments.
4
5 The export manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Export
7
8 ==Operation==
9 The default 'Activate Export' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions will not be called.
10
11 ==Settings==
12 ===Add Descriptive Extension===
13 Default is off.
14
15 When selected, key profile values will be added as an extension to the gcode file.  For example:
16 test.04hx06w_03fill_2cx2r_33EL.gcode
17
18 would mean:
19
20 * . (Carve section.)
21 * 04h = 'Layer Height (mm):' 0.4
22 * x
23 * 06w = 0.6 width i.e. 0.4 times 'Edge Width over Height (ratio):' 1.5
24 * _ (Fill section.)
25 * 03fill = 'Infill Solidity (ratio):' 0.3
26 * _ (Multiply section; if there is one column and one row then this section is not shown.)
27 * 2c = 'Number of Columns (integer):' 2
28 * x
29 * 2r = 'Number of Rows (integer):' 2.
30 * _ (Speed section.)
31 * 33EL = 'Feed Rate (mm/s):' 33.0 and 'Flow Rate Setting (float):' 33.0.  If either value has a positive value after the decimal place then this is also shown, but if it is zero it is hidden.  Also, if the values differ (which they shouldn't with 5D volumetrics) then each should be displayed separately.  For example, 35.2E30L = 'Feed Rate (mm/s):' 35.2 and 'Flow Rate Setting (float):' 30.0.
32
33 ===Add Profile Extension===
34 Default is off.
35
36 When selected, the current profile will be added to the file extension.  For example:
37 test.my_profile_name.gcode
38
39 ===Add Timestamp Extension===
40 Default is off.
41
42 When selected, the current date and time is added as an extension in format YYYYmmdd_HHMMSS (so it is sortable if one has many files).  For example:
43 test.my_profile_name.20110613_220113.gcode
44
45 ===Also Send Output To===
46 Default is empty.
47
48 Defines the output name for sending to a file or pipe.  A common choice is stdout to print the output in the shell screen.  Another common choice is stderr.  With the empty default, nothing will be done.  If the value is anything else, the output will be written to that file name.
49
50 ===Analyze Gcode===
51 Default is on.
52
53 When selected, the penultimate gcode will be sent to the analyze plugins to be analyzed and viewed.
54
55 ===Comment Choice===
56 Default is 'Delete All Comments'.
57
58 ====Do Not Delete Comments====
59 When selected, export will not delete comments.  Crafting comments slow down the processing in many firmware types, which leads to pauses and therefore a lower quality print.
60  
61 ====Delete Crafting Comments====
62 When selected, export will delete the time consuming crafting comments, but leave the initialization comments.  Since the crafting comments are deleted, there are no pauses during extrusion.  The remaining initialization comments provide some useful information for the analyze tools.
63
64 ====Delete All Comments====
65 When selected, export will delete all comments.  The comments are not necessary to run a fabricator.  Some printers do not support comments at all so the safest way is choose this option.
66
67 ===Export Operations===
68 Export presents the user with a choice of the export plugins in the export_plugins folder.  The chosen plugin will then modify the gcode or translate it into another format.  There is also the "Do Not Change Output" choice, which will not change the output.  An export plugin is a script in the export_plugins folder which has the getOutput function, the globalIsReplaceable variable and if it's output is not replaceable, the writeOutput function.
69
70 ===File Extension===
71 Default is gcode.
72
73 Defines the file extension added to the name of the output file.  The output file will be named as originalname_export.extension so if you are processing XYZ.stl the output will by default be XYZ_export.gcode
74  
75 ===Name of Replace File===
76 Default is replace.csv.
77
78 When export is exporting the code, if there is a tab separated file  with the name of the "Name of Replace File" setting, it will replace the string in the first column by its replacement in the second column.  If there is nothing in the second column, the first column string will be deleted, if this leads to an empty line, the line will be deleted.  If there are replacement columns after the second, they will be added as extra lines of text.  There is an example file replace_example.csv to demonstrate the tab separated format, which can be edited in a text editor or a spreadsheet.
79
80 Export looks for the alteration file in the alterations folder in the .skeinforge folder in the home directory.  Export does not care if the text file names are capitalized, but some file systems do not handle file name cases properly, so to be on the safe side you should give them lower case names.  If it doesn't find the file it then looks in the alterations folder in the skeinforge_plugins folder.
81
82 ===Save Penultimate Gcode===
83 Default is off.
84
85 When selected, export will save the gcode file with the suffix '_penultimate.gcode' just before it is exported.  This is useful because the code after it is exported could be in a form which the viewers can not display well.
86
87 ==Examples==
88 The following examples export the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and export.py.
89
90 > python export.py
91 This brings up the export dialog.
92
93 > python export.py Screw Holder Bottom.stl
94 The export tool is parsing the file:
95 Screw Holder Bottom.stl
96 ..
97 The export tool has created the file:
98 .. Screw Holder Bottom_export.gcode
99
100 """
101
102 from __future__ import absolute_import
103
104 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
105 from fabmetheus_utilities import archive
106 from fabmetheus_utilities import euclidean
107 from fabmetheus_utilities import gcodec
108 from fabmetheus_utilities import settings
109 from skeinforge_application.skeinforge_utilities import skeinforge_analyze
110 from skeinforge_application.skeinforge_utilities import skeinforge_craft
111 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
112 from skeinforge_application.skeinforge_utilities import skeinforge_profile
113 import cStringIO
114 import os
115 import sys
116 import time
117
118
119 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
120 __credits__ = 'Gary Hodgson <http://garyhodgson.com/reprap/2011/06/hacking-skeinforge-export-module/>'
121 __date__ = '$Date: 2008/21/04 $'
122 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
123
124
125 def getCraftedTextFromText(gcodeText, repository=None):
126         'Export a gcode linear move text.'
127         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'export'):
128                 return gcodeText
129         if repository == None:
130                 repository = settings.getReadRepository(ExportRepository())
131         if not repository.activateExport.value:
132                 return gcodeText
133         return ExportSkein().getCraftedGcode(repository, gcodeText)
134
135 def getDescriptionCarve(lines):
136         'Get the description for carve.'
137         descriptionCarve = ''
138         layerThicknessString = getSettingString(lines, 'carve', 'Layer Height')
139         if layerThicknessString != None:
140                 descriptionCarve += layerThicknessString.replace('.', '') + 'h'
141         edgeWidthString = getSettingString(lines, 'carve', 'Edge Width over Height')
142         if edgeWidthString != None:
143                 descriptionCarve += 'x%sw' % str(float(edgeWidthString) * float(layerThicknessString)).replace('.', '')
144         return descriptionCarve
145
146 def getDescriptionFill(lines):
147         'Get the description for fill.'
148         activateFillString = getSettingString(lines, 'fill', 'Activate Fill')
149         if activateFillString == None or activateFillString == 'False':
150                 return ''
151         infillSolidityString = getSettingString(lines, 'fill', 'Infill Solidity')
152         return '_' + infillSolidityString.replace('.', '') + 'fill'
153
154 def getDescriptionMultiply(lines):
155         'Get the description for multiply.'
156         activateMultiplyString = getSettingString(lines, 'multiply', 'Activate Multiply')
157         if activateMultiplyString == None or activateMultiplyString == 'False':
158                 return ''
159         columnsString = getSettingString(lines, 'multiply', 'Number of Columns')
160         rowsString = getSettingString(lines, 'multiply', 'Number of Rows')
161         if columnsString == '1' and rowsString == '1':
162                 return ''
163         return '_%scx%sr' % (columnsString, rowsString)
164
165 def getDescriptionSpeed(lines):
166         'Get the description for speed.'
167         activateSpeedString = getSettingString(lines, 'speed', 'Activate Speed')
168         if activateSpeedString == None or activateSpeedString == 'False':
169                 return ''
170         feedRateString = getSettingString(lines, 'speed', 'Feed Rate')
171         flowRateString = getSettingString(lines, 'speed', 'Flow Rate')
172         if feedRateString == flowRateString:
173                 return '_%sEL' % feedRateString.replace('.0', '')
174         return '_%sE%sL' % (feedRateString.replace('.0', ''), flowRateString.replace('.0', ''))
175
176 def getDescriptiveExtension(gcodeText):
177         'Get the descriptive extension.'
178         lines = archive.getTextLines(gcodeText)
179         return '.' + getDescriptionCarve(lines) + getDescriptionFill(lines) + getDescriptionMultiply(lines) + getDescriptionSpeed(lines)
180
181 def getDistanceGcode(exportText):
182         'Get gcode lines with distance variable added, this is for if ever there is distance code.'
183         lines = archive.getTextLines(exportText)
184         oldLocation = None
185         for line in lines:
186                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
187                 firstWord = None
188                 if len(splitLine) > 0:
189                         firstWord = splitLine[0]
190                 if firstWord == 'G1':
191                         location = gcodec.getLocationFromSplitLine(oldLocation, splitLine)
192                         if oldLocation != None:
193                                 distance = location.distance(oldLocation)
194                         oldLocation = location
195         return exportText
196
197 def getFirstValue(gcodeText, word):
198         'Get the value from the first line which starts with the given word.'
199         for line in archive.getTextLines(gcodeText):
200                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
201                 if gcodec.getFirstWord(splitLine) == word:
202                         return splitLine[1]
203         return ''
204
205 def getNewRepository():
206         'Get new repository.'
207         return ExportRepository()
208
209 def getReplaceableExportGcode(nameOfReplaceFile, replaceableExportGcode):
210         'Get text with strings replaced according to replace.csv file.'
211         replaceLines = settings.getAlterationLines(nameOfReplaceFile)
212         if len(replaceLines) < 1:
213                 return replaceableExportGcode
214         for replaceLine in replaceLines:
215                 splitLine = replaceLine.replace('\\n', '\t').split('\t')
216                 if len(splitLine) > 0:
217                         replaceableExportGcode = replaceableExportGcode.replace(splitLine[0], '\n'.join(splitLine[1 :]))
218         output = cStringIO.StringIO()
219         gcodec.addLinesToCString(output, archive.getTextLines(replaceableExportGcode))
220         return output.getvalue()
221
222 def getSelectedPluginModule( plugins ):
223         'Get the selected plugin module.'
224         for plugin in plugins:
225                 if plugin.value:
226                         return archive.getModuleWithDirectoryPath( plugin.directoryPath, plugin.name )
227         return None
228
229 def getSettingString(lines, procedureName, settingNameStart):
230         'Get the setting value from the lines, return None if there is no setting starting with that name.'
231         settingNameStart = settingNameStart.replace(' ', '_')
232         for line in lines:
233                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
234                 firstWord = None
235                 if len(splitLine) > 0:
236                         firstWord = splitLine[0]
237                 if firstWord == '(<setting>':
238                         if len(splitLine) > 4:
239                                 if splitLine[1] == procedureName and splitLine[2].startswith(settingNameStart):
240                                         return splitLine[3]
241                 elif firstWord == '(</settings>)':
242                         return None
243         return None
244
245 def sendOutputTo(outputTo, text):
246         'Send output to a file or a standard output.'
247         if outputTo.endswith('stderr'):
248                 sys.stderr.write(text)
249                 sys.stderr.write('\n')
250                 sys.stderr.flush()
251                 return
252         if outputTo.endswith('stdout'):
253                 sys.stdout.write(text)
254                 sys.stdout.write('\n')
255                 sys.stdout.flush()
256                 return
257         archive.writeFileText(outputTo, text)
258
259 def getOutput(fileName):
260         'Export a gcode linear move file.'
261         if fileName == '':
262                 return None
263         repository = ExportRepository()
264         settings.getReadRepository(repository)
265         startTime = time.time()
266         print('File ' + archive.getSummarizedFileName(fileName.encode('ascii', 'replace')) + ' is being chain exported.')
267         gcodeText = gcodec.getGcodeFileText(fileName, '')
268         procedures = skeinforge_craft.getProcedures('export', gcodeText)
269         gcodeText = skeinforge_craft.getChainTextFromProcedures(fileName, procedures[: -1], gcodeText)
270         if gcodeText == '':
271                 return None
272         fileNamePenultimate = fileName[: fileName.rfind('.')] + '_penultimate.gcode'
273         if repository.savePenultimateGcode.value:
274                 archive.writeFileText(fileNamePenultimate, gcodeText)
275                 print('The penultimate file is saved as ' + archive.getSummarizedFileName(fileNamePenultimate))
276         exportGcode = getCraftedTextFromText(gcodeText, repository)
277         replaceableExportGcode = None
278         selectedPluginModule = getSelectedPluginModule(repository.exportPlugins)
279         if selectedPluginModule is None:
280                 replaceableExportGcode = exportGcode
281         else:
282                 if selectedPluginModule.globalIsReplaceable:
283                         replaceableExportGcode = selectedPluginModule.getOutput(exportGcode)
284                 #else:
285                 #       selectedPluginModule.writeOutput(outputFilename, exportGcode)
286         if replaceableExportGcode is not None:
287                 replaceableExportGcode = getReplaceableExportGcode(repository.nameOfReplaceFile.value, replaceableExportGcode)
288         if repository.alsoSendOutputTo.value != '':
289                 if replaceableExportGcode == None:
290                         replaceableExportGcode = selectedPluginModule.getOutput(exportGcode)
291                 sendOutputTo(repository.alsoSendOutputTo.value, replaceableExportGcode)
292         print('It took %s to export the file.' % euclidean.getDurationString(time.time() - startTime))
293         return replaceableExportGcode
294
295 class ExportRepository(object):
296         'A class to handle the export settings.'
297         def __init__(self):
298                 'Set the default settings, execute title & settings fileName.'
299                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.export.html', self)
300                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Export', self, '')
301                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Export')
302                 self.activateExport = settings.BooleanSetting().getFromValue('Activate Export', self, True)
303                 self.alsoSendOutputTo = settings.StringSetting().getFromValue('Also Send Output To:', self, '')
304                 self.analyzeGcode = settings.BooleanSetting().getFromValue('Analyze Gcode', self, True)
305                 self.commentChoice = settings.MenuButtonDisplay().getFromName('Comment Choice:', self)
306                 self.doNotDeleteComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Do Not Delete Comments', self, True)
307                 self.deleteCraftingComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Delete Crafting Comments', self, False)
308                 self.deleteAllComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Delete All Comments', self, False)
309                 exportPluginsFolderPath = archive.getAbsoluteFrozenFolderPath(archive.getCraftPluginsDirectoryPath('export.py'), 'export_plugins')
310                 exportStaticDirectoryPath = os.path.join(exportPluginsFolderPath, 'static_plugins')
311                 exportPluginFileNames = archive.getPluginFileNamesFromDirectoryPath(exportPluginsFolderPath)
312                 exportStaticPluginFileNames = archive.getPluginFileNamesFromDirectoryPath(exportStaticDirectoryPath)
313                 self.exportLabel = settings.LabelDisplay().getFromName('Export Operations: ', self)
314                 self.exportPlugins = []
315                 exportLatentStringVar = settings.LatentStringVar()
316                 self.doNotChangeOutput = settings.RadioCapitalized().getFromRadio(exportLatentStringVar, 'Do Not Change Output', self, False)
317                 self.doNotChangeOutput.directoryPath = None
318                 allExportPluginFileNames = exportPluginFileNames + exportStaticPluginFileNames
319                 for exportPluginFileName in allExportPluginFileNames:
320                         exportPlugin = None
321                         default = False
322                         if exportPluginFileName == "gcode_small":
323                                 default = True
324                         if exportPluginFileName in exportPluginFileNames:
325                                 path = os.path.join(exportPluginsFolderPath, exportPluginFileName)
326                                 exportPlugin = settings.RadioCapitalizedButton().getFromPath(exportLatentStringVar, exportPluginFileName, path, self, default)
327                                 exportPlugin.directoryPath = exportPluginsFolderPath
328                         else:
329                                 exportPlugin = settings.RadioCapitalized().getFromRadio(exportLatentStringVar, exportPluginFileName, self, default)
330                                 exportPlugin.directoryPath = exportStaticDirectoryPath
331                         self.exportPlugins.append(exportPlugin)
332                 self.fileExtension = settings.StringSetting().getFromValue('File Extension:', self, 'gcode')
333                 self.nameOfReplaceFile = settings.StringSetting().getFromValue('Name of Replace File:', self, 'replace.csv')
334                 self.savePenultimateGcode = settings.BooleanSetting().getFromValue('Save Penultimate Gcode', self, False)
335                 self.executeTitle = 'Export'
336
337         def execute(self):
338                 'Export button has been clicked.'
339                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
340                 for fileName in fileNames:
341                         writeOutput(fileName)
342
343
344 class ExportSkein(object):
345         'A class to export a skein of extrusions.'
346         def __init__(self):
347                 self.crafting = False
348                 self.decimalPlacesExported = 2
349                 self.output = cStringIO.StringIO()
350
351         def addLine(self, line):
352                 'Add a line of text and a newline to the output.'
353                 if line != '':
354                         self.output.write(line + '\n')
355
356         def getCraftedGcode( self, repository, gcodeText ):
357                 'Parse gcode text and store the export gcode.'
358                 self.repository = repository
359                 lines = archive.getTextLines(gcodeText)
360                 for line in lines:
361                         self.parseLine(line)
362                 return self.output.getvalue()
363
364         def getLineWithTruncatedNumber(self, character, line, splitLine):
365                 'Get a line with the number after the character truncated.'
366                 numberString = gcodec.getStringFromCharacterSplitLine(character, splitLine)
367                 if numberString == None:
368                         return line
369                 roundedNumberString = euclidean.getRoundedToPlacesString(self.decimalPlacesExported, float(numberString))
370                 return gcodec.getLineWithValueString(character, line, splitLine, roundedNumberString)
371
372         def parseLine(self, line):
373                 'Parse a gcode line.'
374                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
375                 if len(splitLine) < 1:
376                         self.addLine(line)
377                         return
378                 firstWord = splitLine[0]
379                 if firstWord == '(</crafting>)':
380                         self.crafting = False
381                 elif firstWord == '(<decimalPlacesCarried>':
382                         self.decimalPlacesExported = int(splitLine[1]) - 1
383                 if self.repository.deleteAllComments.value or (self.repository.deleteCraftingComments.value and self.crafting):
384                         if firstWord[0] == '(':
385                                 return
386                         else:
387                                 line = line.split(';')[0].split('(')[0].strip()
388                 if firstWord == '(<crafting>)':
389                         self.crafting = True
390                 if firstWord == '(</extruderInitialization>)':
391                         self.addLine(gcodec.getTagBracketedProcedure('export'))
392                 if firstWord != 'G1' and firstWord != 'G2' and firstWord != 'G3' :
393                         self.addLine(line)
394                         return
395                 line = self.getLineWithTruncatedNumber('X', line, splitLine)
396                 line = self.getLineWithTruncatedNumber('Y', line, splitLine)
397                 line = self.getLineWithTruncatedNumber('Z', line, splitLine)
398                 line = self.getLineWithTruncatedNumber('I', line, splitLine)
399                 line = self.getLineWithTruncatedNumber('J', line, splitLine)
400                 line = self.getLineWithTruncatedNumber('R', line, splitLine)
401                 self.addLine(line)
402
403
404 def main():
405         'Display the export dialog.'
406         if len(sys.argv) > 1:
407                 writeOutput(' '.join(sys.argv[1 :]))
408         else:
409                 settings.startMainLoopFromConstructor(getNewRepository())
410
411 if __name__ == '__main__':
412         main()