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