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