chiark / gitweb /
Add minimal travel distance for retraction and option to disable combing and thus...
[cura.git] / Cura / util / sliceEngine.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2 import subprocess
3 import time
4 import math
5 import numpy
6 import os
7 import warnings
8 import threading
9 import traceback
10 import platform
11 import sys
12 import urllib
13 import urllib2
14 import hashlib
15
16 from Cura.util import profile
17 from Cura.util import version
18
19 def getEngineFilename():
20         if platform.system() == 'Windows':
21                 if os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
22                         return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
23                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
24         if hasattr(sys, 'frozen'):
25                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
26         if os.path.isfile('/usr/bin/CuraEngine'):
27                 return '/usr/bin/CuraEngine'
28         if os.path.isfile('/usr/local/bin/CuraEngine'):
29                 return '/usr/local/bin/CuraEngine'
30         return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
31
32 def getTempFilename():
33         warnings.simplefilter('ignore')
34         ret = os.tempnam(None, "Cura_Tmp")
35         warnings.simplefilter('default')
36         return ret
37
38 class Slicer(object):
39         def __init__(self, progressCallback):
40                 self._process = None
41                 self._thread = None
42                 self._callback = progressCallback
43                 self._binaryStorageFilename = getTempFilename()
44                 self._exportFilename = getTempFilename()
45                 self._progressSteps = ['inset', 'skin', 'export']
46                 self._objCount = 0
47                 self._sliceLog = []
48                 self._printTimeSeconds = None
49                 self._filamentMM = None
50                 self._modelHash = None
51                 self._id = 0
52
53         def cleanup(self):
54                 self.abortSlicer()
55                 try:
56                         os.remove(self._binaryStorageFilename)
57                 except:
58                         pass
59                 try:
60                         os.remove(self._exportFilename)
61                 except:
62                         pass
63
64         def abortSlicer(self):
65                 if self._process is not None:
66                         try:
67                                 self._process.terminate()
68                         except:
69                                 pass
70                         self._thread.join()
71                 self._thread = None
72
73         def wait(self):
74                 if self._thread is not None:
75                         self._thread.join()
76
77         def getGCodeFilename(self):
78                 return self._exportFilename
79
80         def getSliceLog(self):
81                 return self._sliceLog
82
83         def getID(self):
84                 return self._id
85
86         def getFilamentWeight(self):
87                 #Calculates the weight of the filament in kg
88                 radius = float(profile.getProfileSetting('filament_diameter')) / 2
89                 volumeM3 = (self._filamentMM * (math.pi * radius * radius)) / (1000*1000*1000)
90                 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
91
92         def getFilamentCost(self):
93                 cost_kg = profile.getPreferenceFloat('filament_cost_kg')
94                 cost_meter = profile.getPreferenceFloat('filament_cost_meter')
95                 if cost_kg > 0.0 and cost_meter > 0.0:
96                         return "%.2f / %.2f" % (self.getFilamentWeight() * cost_kg, self._filamentMM / 1000.0 * cost_meter)
97                 elif cost_kg > 0.0:
98                         return "%.2f" % (self.getFilamentWeight() * cost_kg)
99                 elif cost_meter > 0.0:
100                         return "%.2f" % (self._filamentMM / 1000.0 * cost_meter)
101                 return None
102
103         def getPrintTime(self):
104                 if int(self._printTimeSeconds / 60 / 60) < 1:
105                         return '%d minutes' % (int(self._printTimeSeconds / 60) % 60)
106                 if int(self._printTimeSeconds / 60 / 60) == 1:
107                         return '%d hour %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
108                 return '%d hours %d minutes' % (int(self._printTimeSeconds / 60 / 60), int(self._printTimeSeconds / 60) % 60)
109
110         def getFilamentAmount(self):
111                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM) / 1000.0, self.getFilamentWeight() * 1000.0)
112
113         def runSlicer(self, scene):
114                 extruderCount = 1
115                 for obj in scene.objects():
116                         if scene.checkPlatform(obj):
117                                 extruderCount = max(extruderCount, len(obj._meshList))
118
119                 commandList = [getEngineFilename(), '-vv']
120                 for k, v in self._engineSettings(extruderCount).iteritems():
121                         commandList += ['-s', '%s=%s' % (k, str(v))]
122                 commandList += ['-o', self._exportFilename]
123                 commandList += ['-b', self._binaryStorageFilename]
124                 self._objCount = 0
125                 with open(self._binaryStorageFilename, "wb") as f:
126                         hash = hashlib.sha512()
127                         order = scene.printOrder()
128                         if order is None:
129                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
130                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
131
132                                 vertexTotal = 0
133                                 for obj in scene.objects():
134                                         if scene.checkPlatform(obj):
135                                                 for mesh in obj._meshList:
136                                                         vertexTotal += mesh.vertexCount
137
138                                 f.write(numpy.array([vertexTotal], numpy.int32).tostring())
139                                 for obj in scene.objects():
140                                         if scene.checkPlatform(obj):
141                                                 for mesh in obj._meshList:
142                                                         vertexes = (numpy.matrix(mesh.vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
143                                                         vertexes -= obj._drawOffset
144                                                         vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
145                                                         f.write(vertexes.tostring())
146                                                         hash.update(mesh.vertexes.tostring())
147
148                                 commandList += ['#']
149                                 self._objCount = 1
150                         else:
151                                 for n in order:
152                                         obj = scene.objects()[n]
153                                         for mesh in obj._meshList:
154                                                 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
155                                                 s = mesh.vertexes.tostring()
156                                                 f.write(s)
157                                                 hash.update(s)
158                                         pos = obj.getPosition() * 1000
159                                         pos += numpy.array(profile.getMachineCenterCoords()) * 1000
160                                         commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
161                                         commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
162                                         commandList += ['#' * len(obj._meshList)]
163                                         self._objCount += 1
164                         self._modelHash = hash.hexdigest()
165                 if self._objCount > 0:
166                         self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
167                         self._thread.daemon = True
168                         self._thread.start()
169
170         def _watchProcess(self, commandList, oldThread):
171                 if oldThread is not None:
172                         if self._process is not None:
173                                 self._process.terminate()
174                         oldThread.join()
175                 self._id += 1
176                 self._callback(-1.0, False)
177                 try:
178                         self._process = self._runSliceProcess(commandList)
179                 except OSError:
180                         traceback.print_exc()
181                         return
182                 if self._thread != threading.currentThread():
183                         self._process.terminate()
184                 self._callback(0.0, False)
185                 self._sliceLog = []
186                 self._printTimeSeconds = None
187                 self._filamentMM = None
188
189                 line = self._process.stdout.readline()
190                 objectNr = 0
191                 while len(line):
192                         line = line.strip()
193                         if line.startswith('Progress:'):
194                                 line = line.split(':')
195                                 if line[1] == 'process':
196                                         objectNr += 1
197                                 elif line[1] in self._progressSteps:
198                                         progressValue = float(line[2]) / float(line[3])
199                                         progressValue /= len(self._progressSteps)
200                                         progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
201
202                                         progressValue /= self._objCount
203                                         progressValue += 1.0 / self._objCount * objectNr
204                                         try:
205                                                 self._callback(progressValue, False)
206                                         except:
207                                                 pass
208                         elif line.startswith('Print time:'):
209                                 self._printTimeSeconds = int(line.split(':')[1].strip())
210                         elif line.startswith('Filament:'):
211                                 self._filamentMM = int(line.split(':')[1].strip())
212                         else:
213                                 self._sliceLog.append(line.strip())
214                         line = self._process.stdout.readline()
215                 for line in self._process.stderr:
216                         self._sliceLog.append(line.strip())
217                 returnCode = self._process.wait()
218                 try:
219                         if returnCode == 0:
220                                 pluginError = profile.runPostProcessingPlugins(self._exportFilename)
221                                 if pluginError is not None:
222                                         print pluginError
223                                         self._sliceLog.append(pluginError)
224                                 self._callback(1.0, True)
225                         else:
226                                 for line in self._sliceLog:
227                                         print line
228                                 self._callback(-1.0, False)
229                 except:
230                         pass
231                 self._process = None
232
233         def _engineSettings(self, extruderCount):
234                 settings = {
235                         'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
236                         'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
237                         'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
238                         'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
239                         'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
240                         'insetCount': int(profile.calculateLineCount()),
241                         'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
242                         'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
243                         'sparseInfillLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density')) if profile.getProfileSettingFloat('fill_density') > 0 else -1,
244                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
245                         'initialSpeedupLayers': int(4),
246                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
247                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
248                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
249                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
250                         'fanOnLayerNr': int(profile.getProfileSettingFloat('fan_layer')),
251                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
252                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
253                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
254                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
255                         'supportLineWidth': int(profile.getProfileSettingFloat('support_rate') * profile.calculateEdgeWidth() * 1000 / 100),
256                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
257                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
258                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
259                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
260                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
261                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
262                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
263                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
264                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
265                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
266                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
267                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
268
269                         'extruderOffset[1].X': int(profile.getPreferenceFloat('extruder_offset_x1') * 1000),
270                         'extruderOffset[1].Y': int(profile.getPreferenceFloat('extruder_offset_y1') * 1000),
271                         'extruderOffset[2].X': int(profile.getPreferenceFloat('extruder_offset_x2') * 1000),
272                         'extruderOffset[2].Y': int(profile.getPreferenceFloat('extruder_offset_y2') * 1000),
273                         'extruderOffset[3].X': int(profile.getPreferenceFloat('extruder_offset_x3') * 1000),
274                         'extruderOffset[3].Y': int(profile.getPreferenceFloat('extruder_offset_y3') * 1000),
275                         'fixHorrible': 0,
276                 }
277                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
278                         settings['skirtDistance'] = 0
279                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
280                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
281                         settings['skirtDistance'] = 0
282                         settings['skirtLineCount'] = 0
283                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
284                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
285                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
286                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
287                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
288                 else:
289                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
290                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
291
292                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
293                         settings['fixHorrible'] |= 0x01
294                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
295                         settings['fixHorrible'] |= 0x02
296                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
297                         settings['fixHorrible'] |= 0x10
298                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
299                         settings['fixHorrible'] |= 0x04
300
301                 if settings['layerThickness'] <= 0:
302                         settings['layerThickness'] = 1000
303                 if profile.getPreference('gcode_flavor') == 'UltiGCode':
304                         settings['gcodeFlavor'] = 1
305                 return settings
306
307         def _runSliceProcess(self, cmdList):
308                 kwargs = {}
309                 if subprocess.mswindows:
310                         su = subprocess.STARTUPINFO()
311                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
312                         su.wShowWindow = subprocess.SW_HIDE
313                         kwargs['startupinfo'] = su
314                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
315                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
316
317         def submitSliceInfoOnline(self):
318                 if profile.getPreference('submit_slice_information') != 'True':
319                         return
320                 if version.isDevVersion():
321                         return
322                 data = {
323                         'processor': platform.processor(),
324                         'machine': platform.machine(),
325                         'platform': platform.platform(),
326                         'profile': profile.getGlobalProfileString(),
327                         'preferences': profile.getGlobalPreferencesString(),
328                         'modelhash': self._modelHash,
329                         'version': version.getVersion(),
330                 }
331                 try:
332                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
333                         f.read()
334                         f.close()
335                 except:
336                         pass