chiark / gitweb /
Make the platform 10mm smaller if you have dual extrusion, else you hit the screws...
[cura.git] / Cura / util / sliceEngine.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2 import subprocess
3 import time
4 import math
5 import numpy
6 import os
7 import warnings
8 import threading
9 import traceback
10 import platform
11 import sys
12 import urllib
13 import urllib2
14 import hashlib
15
16 from Cura.util import profile
17 from Cura.util import version
18
19 def getEngineFilename():
20         if platform.system() == 'Windows':
21                 if os.path.exists('C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'):
22                         return 'C:/Software/Cura_SteamEngine/_bin/Release/Cura_SteamEngine.exe'
23                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine.exe'))
24         if hasattr(sys, 'frozen'):
25                 return os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../..', 'CuraEngine'))
26         if os.path.isfile('/usr/bin/CuraEngine'):
27                 return '/usr/bin/CuraEngine'
28         if os.path.isfile('/usr/local/bin/CuraEngine'):
29                 return '/usr/local/bin/CuraEngine'
30         return os.path.abspath(os.path.join(os.path.dirname(__file__), '../..', 'CuraEngine'))
31
32 def getTempFilename():
33         warnings.simplefilter('ignore')
34         ret = os.tempnam(None, "Cura_Tmp")
35         warnings.simplefilter('default')
36         return ret
37
38 class Slicer(object):
39         def __init__(self, progressCallback):
40                 self._process = None
41                 self._thread = None
42                 self._callback = progressCallback
43                 self._binaryStorageFilename = getTempFilename()
44                 self._exportFilename = getTempFilename()
45                 self._progressSteps = ['inset', 'skin', 'export']
46                 self._objCount = 0
47                 self._sliceLog = []
48                 self._printTimeSeconds = None
49                 self._filamentMM = None
50                 self._modelHash = None
51                 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):
87                 #Calculates the weight of the filament in kg
88                 radius = float(profile.getProfileSetting('filament_diameter')) / 2
89                 volumeM3 = (self._filamentMM * (math.pi * radius * radius)) / (1000*1000*1000)
90                 return volumeM3 * profile.getPreferenceFloat('filament_physical_density')
91
92         def getFilamentCost(self):
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() * cost_kg, self._filamentMM / 1000.0 * cost_meter)
97                 elif cost_kg > 0.0:
98                         return "%.2f" % (self.getFilamentWeight() * cost_kg)
99                 elif cost_meter > 0.0:
100                         return "%.2f" % (self._filamentMM / 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):
111                 return '%0.2f meter %0.0f gram' % (float(self._filamentMM) / 1000.0, self.getFilamentWeight() * 1000.0)
112
113         def runSlicer(self, scene):
114                 extruderCount = 1
115                 for obj in scene.objects():
116                         if scene.checkPlatform(obj):
117                                 extruderCount = max(extruderCount, len(obj._meshList))
118
119                 commandList = [getEngineFilename(), '-vv']
120                 for k, v in self._engineSettings(extruderCount).iteritems():
121                         commandList += ['-s', '%s=%s' % (k, str(v))]
122                 commandList += ['-o', self._exportFilename]
123                 commandList += ['-b', self._binaryStorageFilename]
124                 self._objCount = 0
125                 with open(self._binaryStorageFilename, "wb") as f:
126                         hash = hashlib.sha512()
127                         order = scene.printOrder()
128                         if order is None:
129                                 pos = numpy.array(profile.getMachineCenterCoords()) * 1000
130                                 commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
131
132                                 vertexTotal = 0
133                                 for obj in scene.objects():
134                                         if scene.checkPlatform(obj):
135                                                 for mesh in obj._meshList:
136                                                         vertexTotal += mesh.vertexCount
137
138                                 f.write(numpy.array([vertexTotal], numpy.int32).tostring())
139                                 for obj in scene.objects():
140                                         if scene.checkPlatform(obj):
141                                                 for mesh in obj._meshList:
142                                                         vertexes = (numpy.matrix(mesh.vertexes, copy = False) * numpy.matrix(obj._matrix, numpy.float32)).getA()
143                                                         vertexes -= obj._drawOffset
144                                                         vertexes += numpy.array([obj.getPosition()[0], obj.getPosition()[1], 0.0])
145                                                         f.write(vertexes.tostring())
146                                                         hash.update(mesh.vertexes.tostring())
147
148                                 commandList += ['#']
149                                 self._objCount = 1
150                         else:
151                                 for n in order:
152                                         obj = scene.objects()[n]
153                                         for mesh in obj._meshList:
154                                                 f.write(numpy.array([mesh.vertexCount], numpy.int32).tostring())
155                                                 s = mesh.vertexes.tostring()
156                                                 f.write(s)
157                                                 hash.update(s)
158                                         pos = obj.getPosition() * 1000
159                                         pos += numpy.array(profile.getMachineCenterCoords()) * 1000
160                                         commandList += ['-m', ','.join(map(str, obj._matrix.getA().flatten()))]
161                                         commandList += ['-s', 'posx=%d' % int(pos[0]), '-s', 'posy=%d' % int(pos[1])]
162                                         commandList += ['#' * len(obj._meshList)]
163                                         self._objCount += 1
164                         self._modelHash = hash.hexdigest()
165                 if self._objCount > 0:
166                         self._thread = threading.Thread(target=self._watchProcess, args=(commandList, self._thread))
167                         self._thread.daemon = True
168                         self._thread.start()
169
170         def _watchProcess(self, commandList, oldThread):
171                 if oldThread is not None:
172                         if self._process is not None:
173                                 self._process.terminate()
174                         oldThread.join()
175                 self._id += 1
176                 self._callback(-1.0, False)
177                 try:
178                         self._process = self._runSliceProcess(commandList)
179                 except OSError:
180                         traceback.print_exc()
181                         return
182                 if self._thread != threading.currentThread():
183                         self._process.terminate()
184                 self._callback(0.0, False)
185                 self._sliceLog = []
186                 self._printTimeSeconds = None
187                 self._filamentMM = None
188
189                 line = self._process.stdout.readline()
190                 objectNr = 0
191                 while len(line):
192                         line = line.strip()
193                         if line.startswith('Progress:'):
194                                 line = line.split(':')
195                                 if line[1] == 'process':
196                                         objectNr += 1
197                                 elif line[1] in self._progressSteps:
198                                         progressValue = float(line[2]) / float(line[3])
199                                         progressValue /= len(self._progressSteps)
200                                         progressValue += 1.0 / len(self._progressSteps) * self._progressSteps.index(line[1])
201
202                                         progressValue /= self._objCount
203                                         progressValue += 1.0 / self._objCount * objectNr
204                                         try:
205                                                 self._callback(progressValue, False)
206                                         except:
207                                                 pass
208                         elif line.startswith('Print time:'):
209                                 self._printTimeSeconds = int(line.split(':')[1].strip())
210                         elif line.startswith('Filament:'):
211                                 self._filamentMM = int(line.split(':')[1].strip())
212                                 if profile.getPreference('gcode_flavor') == 'UltiGCode':
213                                         radius = profile.getProfileSettingFloat('filament_diameter') / 2.0
214                                         self._filamentMM /= (math.pi * radius * radius)
215                         else:
216                                 self._sliceLog.append(line.strip())
217                         line = self._process.stdout.readline()
218                 for line in self._process.stderr:
219                         self._sliceLog.append(line.strip())
220                 returnCode = self._process.wait()
221                 try:
222                         if returnCode == 0:
223                                 pluginError = profile.runPostProcessingPlugins(self._exportFilename)
224                                 if pluginError is not None:
225                                         print pluginError
226                                         self._sliceLog.append(pluginError)
227                                 self._callback(1.0, True)
228                         else:
229                                 for line in self._sliceLog:
230                                         print line
231                                 self._callback(-1.0, False)
232                 except:
233                         pass
234                 self._process = None
235
236         def _engineSettings(self, extruderCount):
237                 settings = {
238                         'layerThickness': int(profile.getProfileSettingFloat('layer_height') * 1000),
239                         'initialLayerThickness': int(profile.getProfileSettingFloat('bottom_thickness') * 1000) if profile.getProfileSettingFloat('bottom_thickness') > 0.0 else int(profile.getProfileSettingFloat('layer_height') * 1000),
240                         'filamentDiameter': int(profile.getProfileSettingFloat('filament_diameter') * 1000),
241                         'filamentFlow': int(profile.getProfileSettingFloat('filament_flow')),
242                         'extrusionWidth': int(profile.calculateEdgeWidth() * 1000),
243                         'insetCount': int(profile.calculateLineCount()),
244                         'downSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_bottom') == 'True' else 0,
245                         'upSkinCount': int(profile.calculateSolidLayerCount()) if profile.getProfileSetting('solid_top') == 'True' else 0,
246                         'sparseInfillLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density')) if profile.getProfileSettingFloat('fill_density') > 0 else -1,
247                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
248                         'initialSpeedupLayers': int(4),
249                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
250                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
251                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
252                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
253                         'fanOnLayerNr': int(profile.getProfileSettingFloat('fan_layer')),
254                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
255                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
256                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
257                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
258                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
259                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
260                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
261                         'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' else -1),
262                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
263                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
264                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
265                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
266                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
267                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
268                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
269                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
270                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
271                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
272                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
273                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
274                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
275
276                         'extruderOffset[1].X': int(profile.getPreferenceFloat('extruder_offset_x1') * 1000),
277                         'extruderOffset[1].Y': int(profile.getPreferenceFloat('extruder_offset_y1') * 1000),
278                         'extruderOffset[2].X': int(profile.getPreferenceFloat('extruder_offset_x2') * 1000),
279                         'extruderOffset[2].Y': int(profile.getPreferenceFloat('extruder_offset_y2') * 1000),
280                         'extruderOffset[3].X': int(profile.getPreferenceFloat('extruder_offset_x3') * 1000),
281                         'extruderOffset[3].Y': int(profile.getPreferenceFloat('extruder_offset_y3') * 1000),
282                         'fixHorrible': 0,
283                 }
284                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
285                         settings['skirtDistance'] = 0
286                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
287                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
288                         settings['skirtDistance'] = 0
289                         settings['skirtLineCount'] = 0
290                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
291                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
292                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
293                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
294                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
295                 else:
296                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
297                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
298                         settings['skirtMinLenght'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
299
300                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
301                         settings['fixHorrible'] |= 0x01
302                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
303                         settings['fixHorrible'] |= 0x02
304                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
305                         settings['fixHorrible'] |= 0x10
306                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
307                         settings['fixHorrible'] |= 0x04
308
309                 if settings['layerThickness'] <= 0:
310                         settings['layerThickness'] = 1000
311                 if profile.getPreference('gcode_flavor') == 'UltiGCode':
312                         settings['gcodeFlavor'] = 1
313                 return settings
314
315         def _runSliceProcess(self, cmdList):
316                 kwargs = {}
317                 if subprocess.mswindows:
318                         su = subprocess.STARTUPINFO()
319                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
320                         su.wShowWindow = subprocess.SW_HIDE
321                         kwargs['startupinfo'] = su
322                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
323                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
324
325         def submitSliceInfoOnline(self):
326                 if profile.getPreference('submit_slice_information') != 'True':
327                         return
328                 if version.isDevVersion():
329                         return
330                 data = {
331                         'processor': platform.processor(),
332                         'machine': platform.machine(),
333                         'platform': platform.platform(),
334                         'profile': profile.getGlobalProfileString(),
335                         'preferences': profile.getGlobalPreferencesString(),
336                         'modelhash': self._modelHash,
337                         'version': version.getVersion(),
338                 }
339                 try:
340                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
341                         f.read()
342                         f.close()
343                 except:
344                         pass