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