chiark / gitweb /
8944b057af500109d285cbfda646034a0d5982d8
[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.getMachineSetting('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                         'infillOverlap': int(profile.getProfileSettingFloat('fill_overlap')),
247                         'initialSpeedupLayers': int(4),
248                         'initialLayerSpeed': int(profile.getProfileSettingFloat('bottom_layer_speed')),
249                         'printSpeed': int(profile.getProfileSettingFloat('print_speed')),
250                         'infillSpeed': int(profile.getProfileSettingFloat('infill_speed')) if int(profile.getProfileSettingFloat('infill_speed')) > 0 else int(profile.getProfileSettingFloat('print_speed')),
251                         'moveSpeed': int(profile.getProfileSettingFloat('travel_speed')),
252                         'fanOnLayerNr': int(profile.getProfileSettingFloat('fan_layer')),
253                         'fanSpeedMin': int(profile.getProfileSettingFloat('fan_speed')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
254                         'fanSpeedMax': int(profile.getProfileSettingFloat('fan_speed_max')) if profile.getProfileSetting('fan_enabled') == 'True' else 0,
255                         'supportAngle': int(-1) if profile.getProfileSetting('support') == 'None' else int(60),
256                         'supportEverywhere': int(1) if profile.getProfileSetting('support') == 'Everywhere' else int(0),
257                         'supportLineDistance': int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('support_fill_rate')) if profile.getProfileSettingFloat('support_fill_rate') > 0 else -1,
258                         'supportXYDistance': int(1000 * profile.getProfileSettingFloat('support_xy_distance')),
259                         'supportZDistance': int(1000 * profile.getProfileSettingFloat('support_z_distance')),
260                         'supportExtruder': 0 if profile.getProfileSetting('support_dual_extrusion') == 'First extruder' else (1 if profile.getProfileSetting('support_dual_extrusion') == 'Second extruder' else -1),
261                         'retractionAmount': int(profile.getProfileSettingFloat('retraction_amount') * 1000) if profile.getProfileSetting('retraction_enable') == 'True' else 0,
262                         'retractionSpeed': int(profile.getProfileSettingFloat('retraction_speed')),
263                         'retractionMinimalDistance': int(profile.getProfileSettingFloat('retraction_min_travel') * 1000),
264                         'retractionAmountExtruderSwitch': int(profile.getProfileSettingFloat('retraction_dual_amount') * 1000),
265                         'minimalExtrusionBeforeRetraction': int(profile.getProfileSettingFloat('retraction_minimal_extrusion') * 1000),
266                         'enableCombing': 1 if profile.getProfileSetting('retraction_combing') == 'True' else 0,
267                         'multiVolumeOverlap': int(profile.getProfileSettingFloat('overlap_dual') * 1000),
268                         'objectSink': int(profile.getProfileSettingFloat('object_sink') * 1000),
269                         'minimalLayerTime': int(profile.getProfileSettingFloat('cool_min_layer_time')),
270                         'minimalFeedrate': int(profile.getProfileSettingFloat('cool_min_feedrate')),
271                         'coolHeadLift': 1 if profile.getProfileSetting('cool_head_lift') == 'True' else 0,
272                         'startCode': profile.getAlterationFileContents('start.gcode', extruderCount),
273                         'endCode': profile.getAlterationFileContents('end.gcode', extruderCount),
274
275                         'extruderOffset[1].X': int(profile.getMachineSettingFloat('extruder_offset_x1') * 1000),
276                         'extruderOffset[1].Y': int(profile.getMachineSettingFloat('extruder_offset_y1') * 1000),
277                         'extruderOffset[2].X': int(profile.getMachineSettingFloat('extruder_offset_x2') * 1000),
278                         'extruderOffset[2].Y': int(profile.getMachineSettingFloat('extruder_offset_y2') * 1000),
279                         'extruderOffset[3].X': int(profile.getMachineSettingFloat('extruder_offset_x3') * 1000),
280                         'extruderOffset[3].Y': int(profile.getMachineSettingFloat('extruder_offset_y3') * 1000),
281                         'fixHorrible': 0,
282                 }
283                 if profile.getProfileSettingFloat('fill_density') == 0:
284                         settings['sparseInfillLineDistance'] = -1
285                 elif profile.getProfileSettingFloat('fill_density') == 100:
286                         settings['sparseInfillLineDistance'] = settings['extrusionWidth']
287                         #Set the up/down skins height to 10000 if we want a 100% filled object.
288                         # This gives better results then normal 100% infill as the sparse and up/down skin have some overlap.
289                         settings['downSkinCount'] = 10000
290                         settings['upSkinCount'] = 10000
291                 else:
292                         settings['sparseInfillLineDistance'] = int(100 * profile.calculateEdgeWidth() * 1000 / profile.getProfileSettingFloat('fill_density'))
293                 if profile.getProfileSetting('platform_adhesion') == 'Brim':
294                         settings['skirtDistance'] = 0
295                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('brim_line_count'))
296                 elif profile.getProfileSetting('platform_adhesion') == 'Raft':
297                         settings['skirtDistance'] = 0
298                         settings['skirtLineCount'] = 0
299                         settings['raftMargin'] = int(profile.getProfileSettingFloat('raft_margin') * 1000)
300                         settings['raftLineSpacing'] = int(profile.getProfileSettingFloat('raft_line_spacing') * 1000)
301                         settings['raftBaseThickness'] = int(profile.getProfileSettingFloat('raft_base_thickness') * 1000)
302                         settings['raftBaseLinewidth'] = int(profile.getProfileSettingFloat('raft_base_linewidth') * 1000)
303                         settings['raftInterfaceThickness'] = int(profile.getProfileSettingFloat('raft_interface_thickness') * 1000)
304                         settings['raftInterfaceLinewidth'] = int(profile.getProfileSettingFloat('raft_interface_linewidth') * 1000)
305                 else:
306                         settings['skirtDistance'] = int(profile.getProfileSettingFloat('skirt_gap') * 1000)
307                         settings['skirtLineCount'] = int(profile.getProfileSettingFloat('skirt_line_count'))
308                         settings['skirtMinLength'] = int(profile.getProfileSettingFloat('skirt_minimal_length') * 1000)
309
310                 if profile.getProfileSetting('fix_horrible_union_all_type_a') == 'True':
311                         settings['fixHorrible'] |= 0x01
312                 if profile.getProfileSetting('fix_horrible_union_all_type_b') == 'True':
313                         settings['fixHorrible'] |= 0x02
314                 if profile.getProfileSetting('fix_horrible_use_open_bits') == 'True':
315                         settings['fixHorrible'] |= 0x10
316                 if profile.getProfileSetting('fix_horrible_extensive_stitching') == 'True':
317                         settings['fixHorrible'] |= 0x04
318
319                 if settings['layerThickness'] <= 0:
320                         settings['layerThickness'] = 1000
321                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
322                         settings['gcodeFlavor'] = 1
323                 return settings
324
325         def _runSliceProcess(self, cmdList):
326                 kwargs = {}
327                 if subprocess.mswindows:
328                         su = subprocess.STARTUPINFO()
329                         su.dwFlags |= subprocess.STARTF_USESHOWWINDOW
330                         su.wShowWindow = subprocess.SW_HIDE
331                         kwargs['startupinfo'] = su
332                         kwargs['creationflags'] = 0x00004000 #BELOW_NORMAL_PRIORITY_CLASS
333                 return subprocess.Popen(cmdList, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
334
335         def submitSliceInfoOnline(self):
336                 if profile.getPreference('submit_slice_information') != 'True':
337                         return
338                 if version.isDevVersion():
339                         return
340                 data = {
341                         'processor': platform.processor(),
342                         'machine': platform.machine(),
343                         'platform': platform.platform(),
344                         'profile': profile.getProfileString(),
345                         'preferences': profile.getPreferencesString(),
346                         'modelhash': self._modelHash,
347                         'version': version.getVersion(),
348                 }
349                 try:
350                         f = urllib2.urlopen("http://www.youmagine.com/curastats/", data = urllib.urlencode(data), timeout = 1)
351                         f.read()
352                         f.close()
353                 except:
354                         pass