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