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