chiark / gitweb /
9cd8fc31a8b3a17ca290080adb2339891bd88a17
[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 = [0.0, 0.0]
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, e=0):
87                 #Calculates the weight of the filament in kg
88                 radius = float(profile.getProfileSetting('filament_diameter')) / 2
89                 volumeM3 = (self._filamentMM[e] * (math.pi * radius * radius)) / (1000*1000*1000)
90                 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
91
92         def getFilamentCost(self, e=0):
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(e) * cost_kg, self._filamentMM[e] / 1000.0 * cost_meter)
97                 elif cost_kg > 0.0:
98                         return "%.2f" % (self.getFilamentWeight(e) * cost_kg)
99                 elif cost_meter > 0.0:
100                         return "%.2f" % (self._filamentMM[e] / 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, e=0):
111                 if self._filamentMM[e] == 0.0:
112                         return None
113                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM[e]) / 1000.0, self.getFilamentWeight(e) * 1000.0)
114
115         def runSlicer(self, scene):
116                 extruderCount = 1
117                 for obj in scene.objects():
118                         if scene.checkPlatform(obj):
119                                 extruderCount = max(extruderCount, len(obj._meshList))
120                 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder':
121                         extruderCount = max(extruderCount, 2)
122
123                 commandList = [getEngineFilename(), '-vv']
124                 for k, v in self._engineSettings(extruderCount).iteritems():
125                         commandList += ['-s', '%s=%s' % (k, str(v))]
126                 commandList += ['-o', self._exportFilename]
127                 commandList += ['-b', self._binaryStorageFilename]
128                 self._objCount = 0
129                 with open(self._binaryStorageFilename, "wb") as f:
130                         hash = hashlib.sha512()
131                         order = scene.printOrder()
132                         if order is None:
133                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
134                                 objMin = None
135                                 objMax = None
136                                 for obj in scene.objects():
137                                         if scene.checkPlatform(obj):
138                                                 oMin = obj.getMinimum()[0:2] + obj.getPosition()
139                                                 oMax = obj.getMaximum()[0:2] + obj.getPosition()
140                                                 if objMin is None:
141                                                         objMin = oMin
142                                                         objMax = oMax
143                                                 else:
144                                                         objMin[0] = min(oMin[0], objMin[0])
145                                                         objMin[1] = min(oMin[1], objMin[1])
146                                                         objMax[0] = max(oMax[0], objMax[0])
147                                                         objMax[1] = max(oMax[1], objMax[1])
148                                 pos += (objMin + objMax) / 2.0 * 1000
149                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
150
151                                 vertexTotal = 0
152                                 for obj in scene.objects():
153                                         if scene.checkPlatform(obj):
154                                                 for mesh in obj._meshList:
155                                                         vertexTotal += mesh.vertexCount
156
157                                 f.write(numpy.array([vertexTotal], numpy.int32).tostring())
158                                 for obj in scene.objects():
159                                         if scene.checkPlatform(obj):
160                                                 for mesh in obj._meshList:
161                                                         vertexes = (numpy.matrix(mesh.vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
162                                                         vertexes -= obj._drawOffset
163                                                         vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
164                                                         f.write(vertexes.tostring())
165                                                         hash.update(mesh.vertexes.tostring())
166
167                                 commandList += ['#']
168                                 self._objCount = 1
169                         else:
170                                 for n in order:
171                                         obj = scene.objects()[n]
172                                         for mesh in obj._meshList:
173                                                 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
174                                                 s = mesh.vertexes.tostring()
175                                                 f.write(s)
176                                                 hash.update(s)
177                                         pos = obj.getPosition() * 1000
178                                         pos += numpy.array(profile.getMachineCenterCoords()) * 1000
179                                         commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
180                                         commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
181                                         commandList += ['#' * len(obj._meshList)]
182                                         self._objCount += 1
183                         self._modelHash = hash.hexdigest()
184                 if self._objCount > 0:
185                         self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
186                         self._thread.daemon = True
187                         self._thread.start()
188
189         def _watchProcess(self, commandList, oldThread):
190                 if oldThread is not None:
191                         if self._process is not None:
192                                 self._process.terminate()
193                         oldThread.join()
194                 self._id += 1
195                 self._callback(-1.0, False)
196                 try:
197                         self._process = self._runSliceProcess(commandList)
198                 except OSError:
199                         traceback.print_exc()
200                         return
201                 if self._thread != threading.currentThread():
202                         self._process.terminate()
203                 self._callback(0.0, False)
204                 self._sliceLog = []
205                 self._printTimeSeconds = None
206                 self._filamentMM = [0.0, 0.0]
207
208                 line = self._process.stdout.readline()
209                 objectNr = 0
210                 while len(line):
211                         line = line.strip()
212                         if line.startswith('Progress:'):
213                                 line = line.split(':')
214                                 if line[1] == 'process':
215                                         objectNr += 1
216                                 elif line[1] in self._progressSteps:
217                                         progressValue = float(line[2]) / float(line[3])
218                                         progressValue /= len(self._progressSteps)
219                                         progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
220
221                                         progressValue /= self._objCount
222                                         progressValue += 1.0 / self._objCount * objectNr
223                                         try:
224                                                 self._callback(progressValue, False)
225                                         except:
226                                                 pass
227                         elif line.startswith('Print time:'):
228                                 self._printTimeSeconds = int(line.split(':')[1].strip())
229                         elif line.startswith('Filament:'):
230                                 self._filamentMM[0] = int(line.split(':')[1].strip())
231                                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
232                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
233                                         self._filamentMM[0] /= (math.pi * radius * radius)
234                         elif line.startswith('Filament2:'):
235                                 self._filamentMM[1] = int(line.split(':')[1].strip())
236                                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
237                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
238                                         self._filamentMM[1] /= (math.pi * radius * radius)
239                         else:
240                                 self._sliceLog.append(line.strip())
241                         line = self._process.stdout.readline()
242                 for line in self._process.stderr:
243                         self._sliceLog.append(line.strip())
244                 returnCode = self._process.wait()
245                 try:
246                         if returnCode == 0:
247                                 pluginError = profile.runPostProcessingPlugins(self._exportFilename)
248                                 if pluginError is not None:
249                                         print pluginError
250                                         self._sliceLog.append(pluginError)
251                                 self._callback(1.0, True)
252                         else:
253                                 for line in self._sliceLog:
254                                         print line
255                                 self._callback(-1.0, False)
256                 except:
257                         pass
258                 self._process = None
259
260         def _engineSettings(self, extruderCount):
261                 settings = {
262                         'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
263                         'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
264                         'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
265                         'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
266                         'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
267                         'insetCount': int(profile.calculateLineCount()),
268                         'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
269                         'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
270                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
271                         'initialSpeedupLayers': int(4),
272                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
273                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
274                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
275                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
276                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
277                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
278                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
279                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
280                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
281                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
282                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
283                         'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' else -1),
284                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
285                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
286                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
287                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
288                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
289                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
290                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
291                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
292                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
293                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
294                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
295                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
296                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
297
298                         'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
299                         'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
300                         'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
301                         'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
302                         'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
303                         'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
304                         'fixHorrible': 0,
305                 }
306                 fanFullHeight = int(profile.getProfileSettingFloat('fan_full_height') * 1000)
307                 settings['fanFullOnLayerNr'] = (fanFullHeight - settings['initialLayerThickness'] - 1) / settings['layerThickness'] + 1
308                 if settings['fanFullOnLayerNr'] < 0:
309                         settings['fanFullOnLayerNr'] = 0
310
311                 if profile.getProfileSettingFloat('fill_density') == 0:
312                         settings['sparseInfillLineDistance'] = -1
313                 elif profile.getProfileSettingFloat('fill_density') == 100:
314                         settings['sparseInfillLineDistance'] = settings['extrusionWidth']
315                         #Set the up/down skins height to 10000 if we want a 100% filled object.
316                         # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
317                         settings['downSkinCount'] = 10000
318                         settings['upSkinCount'] = 10000
319                 else:
320                         settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
321                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
322                         settings['skirtDistance'] = 0
323                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
324                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
325                         settings['skirtDistance'] = 0
326                         settings['skirtLineCount'] = 0
327                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
328                         settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
329                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
330                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
331                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
332                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
333                 else:
334                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
335                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
336                         settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
337
338                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
339                         settings['fixHorrible'] |= 0x01
340                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
341                         settings['fixHorrible'] |= 0x02
342                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
343                         settings['fixHorrible'] |= 0x10
344                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
345                         settings['fixHorrible'] |= 0x04
346
347                 if settings['layerThickness'] <= 0:
348                         settings['layerThickness'] = 1000
349                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
350                         settings['gcodeFlavor'] = 1
351                 if profile.getProfileSetting('spiralize') == 'True':
352                         settings['spiralizeMode'] = 1
353                 if profile.getProfileSetting('wipe_tower') == 'True':
354                         settings['enableWipeTower'] = 1
355                 if profile.getProfileSetting('ooze_shield') == 'True':
356                         settings['enableOozeShield'] = 1
357                 return settings
358
359         def _runSliceProcess(self, cmdList):
360                 kwargs = {}
361                 if subprocess.mswindows:
362                         su = subprocess.STARTUPINFO()
363                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
364                         su.wShowWindow = subprocess.SW_HIDE
365                         kwargs['startupinfo'] = su
366                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
367                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
368
369         def submitSliceInfoOnline(self):
370                 if profile.getPreference('submit_slice_information') != 'True':
371                         return
372                 if version.isDevVersion():
373                         return
374                 data = {
375                         'processor': platform.processor(),
376                         'machine': platform.machine(),
377                         'platform': platform.platform(),
378                         'profile': profile.getProfileString(),
379                         'preferences': profile.getPreferencesString(),
380                         'modelhash': self._modelHash,
381                         'version': version.getVersion(),
382                 }
383                 try:
384                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
385                         f.read()
386                         f.close()
387                 except:
388                         pass